I've got two simple useStates.
At one point, they're set, one after another. This happens within a useEffect with [] deps.
canvas.toBlob(blob => {
if (blob) {
setBounds([width, height]);
setUrl(URL.createObjectURL(blob));
} else console.log("no blob");
});
The above code (first set bounds, then url) causes the component to rerender once, with both states set properly.
However, the code below (first url, then bounds):
canvas.toBlob(blob => {
if (blob) {
setUrl(URL.createObjectURL(blob));
setBounds([width, height]);
} else console.log("no blob");
});
makes the component rerender twice. The first render has only the url set, then only on the second render, the bounds are set as well.
Why does changing the order of these two lines change the rerenders?
I think the reason is because you're inside a callback function, React isn't batching updates.
So the order of these states being set actually matters and somehow setting the url before the bounds is triggering an additional render which I cannot make out from the given code.
Related
class RatingsComponent extends Component{
componentDidMount(){
console.log("inside component did MOUNT")
this.props.fetchReviews()
}
shouldComponentUpdate(nextProps){
console.log("inside SHOULD update")
return (nextProps.reviews !== this.props.reviews);
}
componentDidUpdate(prevProps){
if(prevProps.reviews !== this.props.reviews)
{ console.log("inside component did update")
// this.props.fetchReviews() //commented due to infinite looping and hitting API
}
}
render(){
console.log("inside render");
return null;
}
}
fetchReviews() is an action that hits the API through saga and updates reviews state through reducer. Code absolutely works fine with componentDidMount(). I was curious and wanted to handle change in data if we are on the same page. I tried adding actionListener but there was no change. I added componentDidUpdate() as above. It worked but API is being hit infinite times. But the value of prevProps.reviews and this.props.reviews are same. I can't understand why this is happening. Any help is appreciated.
P.s: Can't show the code inside render() due to official concerns, simply returning null also causes same issue.
The issue is in the condition: you are not checking if the list of the reviews contains the same IDs, but you are just saying that if reviews!==previous fire fetch. This means that every time you retrieve a new array of reviews, even if the content is equal, the fetch request is fired again.
The loop is:
UpdateComponent->ComponentUpdated->ConditionIsTrue->FireFetch->UpdateComponent
And condition is always true since you are controlling two arrays in different memory address
Try using a statement like this and see if it solve your issue:
//diff is an array that will contain all the differences between this.props and previousProps
let diff= this.props.reviews.filter(review=> !previousProps.reviews.includes(review));
//if there are differences, call fetch
if(diff.lenght>0){
this.props.fetchReviews()
}
If we compare two objects with the == will not give an appropriate result so better to compare with the object values.
For Example:
shouldComponentUpdate(nextProps){
console.log("inside SHOULD update")
return nextProps.reviews.id !== this.props.reviews.id;
}
In the above example, I have compared it with the id property you can compare any unique property or main property that is important for re-render
I'm guessing fetchReviews updates redux store with new array of objects .... which causes componentDidUpdate to fire ... and then if(prevProps.reviews !== this.props.reviews) will evaluate to true Even if reviews contains same data ... but they're pointing to different locations in memory
I've created a useEffect callback in my functional component whose responsibility is to determine whether the content of a child overflows its container, and if so, to show a "More" link that will allow the container to be expanded.
const cardNode = useRef(null);
useEffect(() => {
if (isOverflown(cardNode.current)) {
setShowExpander(true);
} else {
setShowExpander(false);
}
function isOverflown(element) {
if (element) {
return (
element.scrollHeight > element.clientHeight ||
element.scrollWidth > element.clientWidth
);
}
return false;
}
}, [cardNode.current]);
Not wanting this effect to run at every render, I have put a reference to the DOM node in the list of dependencies, or rather the ref's current member.
I am getting this react-hooks/exhaustive-deps warning:
react-hooks/exhaustive-deps: React Hook useEffect has an unnecessary dependency: 'cardNode.current'. Either exclude it or remove the dependency array. Mutable values like 'cardNode.current' aren't valid dependencies because mutating the
m doesn't re-render the component.
Is my use case a legitimate exception where I shouldn't care about this warning? Is there a better approach to achieve the same objective?
Updated
The reason I cannot leave the dependency list out is that it seems the node's scrollHeight and clientHeight values are updated at some point after mounting. Initially, by logging to the console, I can see that the scrollHeight and clientHeight are identical, even though all the content that overflows the parent container is already there.
Thus, I need the side effect to run again, once the scrollHeight and clientHeight "stabilizes" on its final values in order to set the state of whether I want an expander to show up or not can be accurately determined.
I have a useEffect function that must wait for four values to have their states changed via an API call in a separate useEffect. In essence the tasks must happen synchronously. The values must be pulled from the API and those stateful variables must be set and current before the second useEffect can be called. I am able to get the values to set appropriately and my component to render properly without doing these tasks synchronously, I have a ref which changes from true to false after first render (initRender), however I find the code to be hacky and inefficient due to the fact that the second useEffect still runs four times. Is there a better way to handle this?
//Hook for gathering group data on initial page load
useEffect(() => {
console.log("UseEffect 1 runs once on first render");
(async () => {
const response = await axios.get(`${server}${gPath}/data`);
const parsed = JSON.parse(response.data);
setGroup(parsed.group);
setSites(parsed.sites);
setUsers(parsed.users);
setSiteIDs(parsed.sitesID);
setUserIDs(parsed.usersID);
})();
return function cleanup() {};
}, [gPath]);
//Hook for setting sitesIN and usersIN values after all previous values are set
useEffect(() => {
console.log("This runs 4 times");
if (
!initRender &&
sites?.length &&
users?.length &&
userIDs !== undefined &&
siteIDs !== undefined
) {
console.log("This runs 1 time");
setSitesIN(getSitesInitialState());
setUsersIN(getUsersInitialState());
setLoading(false);
}
}, [sites, siteIDs, users, userIDs]);
EDIT: The code within the second useEffect's if statement now only runs once BUT the effect still runs 4 times, which still means 4 renders. I've updated the code above to reflect the changes I've made.
LAST EDIT: To anyone that sees this in the future and is having a hard time wrapping your head around updates to stateful variables and when those updates occur, there are multiple approaches to dealing with this, if you know the initial state of your variables like I do, you can set your dependency array in a second useEffect and get away with an if statement to check a change, or multiple changes. Alternatively, if you don't know the initial state, but you do know that the state of the dependencies needs to have changed before you can work with the data, you can create refs and track the state that way. Just follow the examples in the posts linked in the comments.
I LIED: This is the last edit! Someone asked, why can't you combine your different stateful variables (sites and sitesIN for instance) into a single stateful variable so that way they get updated at the same time? So I did, because in my use case that was acceptable. So now I don't need the 2nd useEffect. Efficient code is now efficient!
Your sites !== [] ... does not work as you intend. You need to do
sites?.length && users?.length
to check that the arrays are not empty. This will help to prevent the multiple runs.
locationHistory is always an empty array in the following code:
export function LocationHistoryProvider({ history, children }) {
const [locationHistory, setLocationHistory] = useState([])
useEffect(() => history.listen((location, action) => {
console.log('old state:', locationHistory)
const newLocationHistory = locationHistory ? [...locationHistory, location.pathname] : [location.pathname]
setLocationHistory(newLocationHistory)
}), [history])
return <LocationHistoryContext.Provider value={locationHistory}>{children}</LocationHistoryContext.Provider>
}
console.log always logs []. I have tried doing exactly the same thing in a regular react class and it works fine, which leads me to think I am using hooks wrong.
Any advice would be much appreciated.
UPDATE: Removing the second argument to useEffect ([history]) fixes it. But why? The intention is that this effect will not need to be rerun on every rerender. Becuase it shouldn't need to be. I thought that was the way effects worked.
Adding an empty array also breaks it. It seems [locationHistory] must be added as the 2nd argument to useEffect which stops it from breaking (or no 2nd argument at all). But I am confused why this stops it from breaking? history.listen should run any time the location changes. Why does useEffect need to run again every time locationHistory changes, in order to avoid the aforementioned problem?
P.S. Play around with it here: https://codesandbox.io/s/react-router-ur4d3?fontsize=14 (thanks to lissitz for doing most the leg work there)
You're setting up a listener for the history object, right?
Assuming your history object will remain the same (the very same object reference) across multiple render, this is want you should do:
Set up the listener, after 1st render (i.e: after mounting)
Remove the listener, after unmount
For this you could do it like this:
useEffect(()=>{
history.listen(()=>{//DO WHATEVER});
return () => history.unsubscribe(); // PSEUDO CODE. YOU CAN RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[]); // THIS EMPTY ARRAY MAKES SURE YOUR EFFECT WILL ONLY RUN AFTER 1ST RENDER
But if your history object will change on every render, you'll need to:
cancel the last listener (from the previous render) and
set up a new listener every time your history object changes.
useEffect(()=>{
history.listen(()=>{//DO SOMETHING});
return () => history.unsubscribe(); // PSEUDO CODE. IN THIS CASE, YOU SHOULD RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[history]); // THIS ARRAY MAKES SURE YOUR EFFECT WILL RUN AFTER EVERY RENDER WITH A DIFFERENT `history` OBJECT
NOTE: setState functions are guaranteed to be the same instance across every render. So they don't need to be in the dependency array.
But if you want to access the current state inside of your useEffect. You shouldn't use it directly like you did with the locationHistory (you can, but if you do, you'll need to add it to the dependency array and your effect will run every time it changes). To avoid accessing it directly and adding it to the dependency array, you can do it like this, by using the functional form of the setState method.
setLocationHistory((prevState) => {
if (prevState.length > 0) {
// DO WHATEVER
}
return SOMETHING; // I.E.: SOMETHING WILL BE YOUR NEW STATE
});
What I need to do is to setState with a value, then send data to a children by props, but I would like "state" to forget about that after doing this once.
this.setState({
animateId: 15 //or any number of id
});
then later
if(this.state.animateId){
let idToSent = this.state.animateId;
}
<Items <...different props> animate={idToSent}/> //once after settingState - 15,
// then undefined or whatever with every next time
and I would want to take this value once, send it via props, and forget about it, either way.
Is there any way to do that, beside just altering that one value in state again, because that would cause unnecesary rendering ??
We can set it to undefined like this after our task is complete
this.setState({ doSomething: undefined });
I would let the Item component keep track of when and when not to initiate the animation based on comparing the latest props with the local state.
I don't know exactly how you are triggering the animation, but it sound like you could do something like this in the Item component
const { animateId } = this.props;
if (animateId !== this.state.animateId) {
// Whatever code that starts your animation...
// Then either update the state right away to signal the last animation
// that was triggered, or if it can't be done here, then set it in a
// callback from when the animation is done.
this.setState({ animateId });
}
Just keep in mind that if this code is executed during the render phase, then it probably won't let you call setState.