state is updating inside the useffect() but component not re- rendering - reactjs

I am fetching Image URL list from a getUserCollection function which returns promise, initially state is empty in the console but once it gets the response from function,state is being updated in the console but components doesn't re-render for the updated state. I know that If i make a separate component and pass the state as prop then it sure gonna re-render for the updated state. But why it doesn't happen inside the same component? Can anybody help? Also when i pass state(userCollection) as second argument inside useEffect then its running infinite times.
import React, { useState, useEffect } from "react";
import { getUserCollection } from "../../firebase/firebase";
const CollectionPage = ({ userAuth }) => {
const [userCollection, setUserCollection] = useState([]);
useEffect(() => {
getUserCollection(userAuth.uid)
.then((response) => {
if (response) setUserCollection(response);
})
.catch((error) => console.log(error));
}, []);
console.log("updating???", userCollection); //its updating in console
return (
<main className="container">
<h3>collection page</h3>
{userCollection.map((url) => (
<div class="card-columns">
<div class="card">
<img
src={ url} // its not updating
class="card-img-top"
alt="user-collection"
width="100"
/>
</div>
</div>
))}
</main>
);
};
export default CollectionPage;

https://reactjs.org/docs/hooks-effect.html
"If you pass an empty array ([]), the props and state inside the effect will always have their initial values. While passing [] as the second argument is closer to the familiar componentDidMount and componentWillUnmount mental model, there are usually better solutions to avoid re-running effects too often. Also, don’t forget that React defers running useEffect until after the browser has painted, so doing extra work is less of a problem."
The 2nd argument is what triggers your component to rerender, setting that to your state variable userCollection is triggering your useEffect to run each time the state updates - causing an infinite loop because your useEffect updates the state.
What do you want to actually trigger the rerender? If it's supposed to be blank at first, and then only run once to load the images, you could change it to something like this:
useEffect(() => {
if(!userCollection){
getUserCollection(userAuth.uid)
.then((response) => {
if (response) setUserCollection(response);
})
.catch((error) => console.log(error));
}
}, [userCollection]);
With this logic, the useEffect will only update the state when it is empty. If you're expecting continuous rerenders, you should create a variable that updates when the rerenders should occur.

Related

React component does re-render, however effect does not run whereas it should

This is my Component:
function Test() {
const [data, setData]=useState<Array<string>>([]);
console.log('Hello:', data);
useEffect(()=>{
console.log('data: ', data)
}, [data])
const test1 = () => {
setData(data.concat('a'));
}
const test2 = () => {
setData(data);
}
return (
<>
<button onClick={test1}>Button one</button>
<button onClick={test2}>Button two</button>
</>
);
}
Everything works fine when clicking Button one. Component does re-render and effect does run. However, what happens with Button two is something I cannot explain:
If clicking Button two right after Button one, the Component does re-render but effect does not run. That makes no sense since React is using Object.is comparison for deciding if it should re-render/run the effect. How does this comparison produce different results among useState and useEffect? At first it decides to re-render, that means state value data has changed. How is that true with setData(data)? Then, it decides not to run the effect, that means there is no change in data dependency. Obviously, there is a contradiction among the above two decisions...
If clicking Button two for a second time (after having clicked on Button one), then nothing happens which does make sense absolutely.
Could someone explain the above behaviour?
If the new state is the same as the current state, as you mentioned by Object.is React will skip the re-render but as mentioned in the React useState docs React might still call the component.
Although in some cases React may still need to call your component before skipping the children, it shouldn’t affect your code.
This results in running the console.log with the "Hello: ", data values.
And so, React does not actually re-render the component.
We can see this with a useEffect with no dependency array which acccording to the React useEffect docs should run every re-render.
Effect will re-run after every re-render of the component.
useEffect(() => {
console.warn("Render");
});
As you can see this is not the case.
const {useState, useEffect, Fragment} = React;
function Test() {
const [data, setData] = useState([]);
console.log("Hello:", data);
useEffect(() => {
console.warn("Render");
});
useEffect(() => {
console.log("data: ", data);
}, [data]);
const test1 = () => {
setData(data.concat("a"));
};
const test2 = () => {
setData(data);
};
return (
<Fragment>
<button onClick={test1}>Button one</button>
<button onClick={test2}>Button two</button>
</Fragment>
);
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<Test />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
what happened is when you use setData(data); the Component Re-renders no matter if the Data has actually changed or not, but when it comes to useEffect the way it compares values it takes the old values and compares it with the new ones if the values have changed it will execute what inside it otherwise it won't, this behavior will also happen if the data is an object it will check each value inside it to decide whether the object has changed or not

Why is my boolean state value not toggling?

I know there are other articles and posts on this topic and almost all of them say to use the ! operator for a Boolean state value. I have used this method before but for the life of me I can not toggle this Boolean value.
import { useState } from 'react';
const [playerTurn, setPlayerTurn] = useState(true);
const changePlayerTurn = () => {
console.log(playerTurn); // returns true
setPlayerTurn(!playerTurn);
console.log(playerTurn); // also returns true
};
changePlayerTurn();
I have also tried setPlayerTurn(current => !current), commenting out the rest of my code to avoid interference, and restarted my computer in case that would help but I am still stuck with this issue.
Can anyone point out why this is not working?
The setPlayerTurn method queues your state change (async) so reading the state directly after will provide inconsistent results.
If you use your code correctly in a react component you will see that playerTurn has changed on the next render
You creating a async function, to solve this you can create a button in your component, which will run the function and you can use the "useEffect" hook to log every time the boolean changes... so you can see the changes taking place over time, like this:
import React, { useEffect } from "react";
import { useState } from "react";
const Player = () => {
const [playerTurn, setPlayerTurn] = useState(true);
useEffect(() => {
console.log(playerTurn);
}, [playerTurn]);
return <button onClick={() => setPlayerTurn(!playerTurn)}>change player turn</button>;
};
export default Player;
This is happening because setPlayerTurn is async function.
You can use another hook useEffect() that runs anytime some dependencies update, in this case your playerTurn state.
export default YourComponent = () => {
const [playerTurn, setPlayerTurn] = useState(true);
useEffect(() => {
console.log('playerTurn: ', playerTurn);
}, [playerTurn]);
const changePlayerTurn = () => {
setPlayerTurn(!playerTurn);
}
return (
<button onClick={changePlayerTurn}>Click to change player turn</button>
);
}
Basically whenever you use setState React keeps a record that it needs to update the state. And it will do some time in the future (usually it takes milliseconds). If you console.log() right after updating your state, your state has yet to be updated by React.
So you need to "listen" to changes on your state using useEffect().
useEffect() will run when your component is first mounted, and any time the state in the dependencies array is updated.
The value of the state only changes after the render. You can test this like:
// Get a hook function
const Example = ({title}) => {
const [playerTurn, setPlayerTurn] = React.useState(true);
React.useEffect(() => {
console.log("PlayerTurn changed to", playerTurn);
}, [playerTurn]);
console.log("Rendering...")
return (<div>
<p>Player turn: {playerTurn.toString()}</p>
<button onClick={() => setPlayerTurn(!playerTurn)}>Toggle PlayerTurn</button>
</div>);
};
// Render it
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
The callback inside the useEffect runs during the component mount and when one of the values inside the second argument, the dependecy array, changes. The depency here is playerTurn. When it changes the console will log.
As you will see, before this happens, the "Rendering..." log will appear.

Explanation needed: getting data from API with useEffect hook and get name

const [ countries, setCountries ] = useState([])
const hook = () => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
setCountries(response.data)
})
}
useEffect(hook, [])
This one below doesn't work:
//Uncaught TypeError: Cannot read property 'name' of undefined
console.log(countries[1].name)
This one below does work:
<ul>
{countries.map(country => (
<li>{country.name}</li>
))}
</ul>
Any ide why one method of printing name does work, while the other doesn't?
Coz you can loop through the empty array, but you can't access the index which is not available yet
// So if
countries = []
// this will not throw error
{countries.map(country => (
<li>{country.name}</li>
))}
// but this will
console.log(countries[1].name)
// if you want to check try to run this
console.log(countries.length ? countries[1].name : "not available yer");
The usage of useEffect hook notifies React that component has to perform some side-effects(passed as a callback function to the hook) after it has been rendered, The default behavior of useEffect will run both after the first render and after every update, but when an empty array is passed as a dependency the side-effect will be performed only once after the component has been mounted for the first time.
In the case above useEffect(hook, []) the callback hook will be called after the component has mounted for the first time, which means the component will render with the initial state on it's first render which is an empty array ([]).
That is why when you try to access countries[1].name it errors out, because the value of countries is still an empty array on the first render.
const [ countries, setCountries ] = useState([])
const hook = () => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
setCountries(response.data)
})
}
useEffect(hook, [])
// can not use index expression to get the first element because
// the value of countries is still an empty array on first render
// it only gets populated when axios.get call is succesful inside the
// callback in useEffect hook after the component has mounted for the first time
console.log(countries[1].name)
Solution
Check for the length of the array before trying to get the first element,
if (countries.length) {
console.log(countries[1].name)
}
P.S.- You should be using a .catch block for handling the error when the API call fails.
There is an example solution for a type of request like this in the React document:
https://reactjs.org/docs/hooks-effect.html
The hooks provided by React are for the most part, asynchronous functions provided by React, to help manage the loading of data, presenting it to the DOM, and dealing with updates. The useEffect behaves in a similar way to componentHasLoaded, where the hook is triggered once the functional component has rendered, and the DOM has been loaded, but it may not have been presented to the user yet. It's important to remember this when working with useEffect. useState is another asynchronous hook, but it provides access to the state property of the hook after it has been instantiated, and won't immediately trigger a re-render of the component, unless the data is updated.
The reason you get an undefined error when you attempt to access console.log(countries[1].name) is because the array at that point is still empty.
I'll explain in code:
const myComponent = () => {
// initialise countries: []
const [ countries, setCountries ] = useState([])
const hook = () => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
// This is allow you to see the DOM change after the effect has run
setTimeout(() => setCountries(response.data), 5000);
})
}
// Tell react to run useEffect once the component is loaded
useEffect(hook, [])
// Display data
return (
<p>Countries: {countries.length}<p>
);
};
Because useEffect is an asynchronous function, it doesn't block the execution of the function and the rendering of the DOM, but refreshes the DOM once useEffect is completed. In this case, you are setting the country list, based on the result of the useEffect function.
The useEffect function will still trigger, you will have access to the state, and the function will re-render when the state is updated.
See codepen example:
https://codepen.io/jmitchell38488/pen/OJMXZPv

React empty useEffect

Even though I'm working with React for some time, I can't figure this out. Why is the empty UseEffect causing component to re-render? The thing is I get two console.logs. Tnx!
function App() {
useEffect(() => {
}, [])
console.log('render')
return (
<h1>Hello world</h1>
);
}
Empty useEffect subscribed to all the component's props.
useEffect(() => console.log('rerendered due to new props'));
Any useEffect works at least once after first component's render. So you can use it like componentDidMount.
If you want to use it like componentDidMount only, you have to pass an empty array as the second param.
useEffect(() => console.log('after first render'), []);
Subscribe to a specific prop:
useEffect(() => console.log(' after first render or message updated', props.message), [props.message]);

ReactJS: Empty component's old fetched data before fetching again

I am fetching remote data from my React component. When data is ready, child components are rendered. While data is loading, the 'Data is loading ....' text is displayed.
When the component is rendered for the second time due to the props change, I set the previous data to null in order to show that new data is loading.
const List = (props) => {
const [items, setItems] = useState(null);
useEffect(() => {
setItems(null);
fetch(`http://some_url_to_fetch_items.com?PAGE=${props.page}`)
.then((data) => {
setItems(data);
})
}, [props.page]);
if (!items) {
return "Data is loading ......."
}
return (
<ul>
{items.map(item => (
<li>{item}</li>
))}
</ul>
)
}
The problem of this approach is that when the component is rendered for the second time the setItems(null); code is not executed immediately (probably because useEffect is executed asynchronously) and the component re-renderes 3 times instead of expected 2:
1-st re-render because of the props change (BUT with old data, since setItems(null) is executed too late)
2-nd re-render after setItems(null) is finally executed
3-rd re-render after data is fetched
I understand what the problem with my approach is. But I don't see another way.
It is mandatory to use hooks.
Any ideas?
A quick fix would be to add a key prop to your List:
<List key={page} page={page} />
When the value for page changes, the List component will be unmounted, and a new one rendered. The new one will not have any previous data in it, so you will only get 2 renders instead of 3.
This means you don't have to set previous data to null anymore.
If you do this, you'll need to check that the component is still mounted in your useEffect before calling setItems, otherwise you'll be trying to set state on an unmounted component. You can write:
if(setItems) {
setItems(data)
}
Changing the key prop is a bit hacky though, so I would investigate other ways to solve your issue.
Maybe you should pass the data into the list instead of the list being responsible for fetching data, and have the page state and data both inside the parent component?
For now though, changing the key using the page should work.
use this method :
useEffect(() => {
fetch(`http://some_url_to_fetch_items.com?PAGE=${props.page}`)
.then((data) => {
setItems(data);
})
}, []);
return (
<ul>
{items? items.map(item => (
<li key={item.id}>{item}</li>
)):"Data is Loading..."}
</ul>
)

Resources