React warning Maximum update depth exceeded - reactjs

This is a follow up question to this question which is the nearest to my issue:
Infinite loop in useEffect
I am creating a small React.js app to study the library. I'm getting this warning:
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
I got a functional component, in which there is this code:
const propertiesMap2 = new Map([ //There is also propertiesMap1 which has the same structure
["TITLE4",
{
propertyValues: {
myProperty10 : "myVal1",
myProperty11 : "myVal2",
myProperty12 : "myVal3",
},
isOpen: true
}
],
["TITLE5",
{
propertyValues: {
myProperty13 : "myVal4",
myProperty14 : "myVal5",
myProperty15 : "myVal6",
},
isOpen: true
}
],
["TITLE6",
{
propertyValues:{
myProperty16 : "myVal7",
myProperty17 : "myVal8",
myProperty18 : "myVal9",
},
isOpen: true
}
]
]);
const [properties, setPropertiesMapFunc] = useState(new Map());
useEffect(()=>
{
let mapNum = Number(props.match.params.id);
setPropertiesMapFunc(mapNum === 1 ?propertiesMap1 : propertiesMap2);
}, [properties]);
The correct properties map is chosen each time, but like I said I get this error. Why do I get it, if the propertiesMap is constant without anything changing, and properties was passed as a parameter to setEffect, so I thought it would only re render when something there changes..

Because you are creating the map objects inside of your component function they will be recreated on every render. Because of that your effect will set a new map as the new state which will in turn trigger another re-render and your effect being called again which leads to an infinite update loop.
You can move the definition of your map objects outside of your component to fix this.

properties is a Map, and when passed in dependency array to useEffect, it will be recreated on every render, and when compared, it will always be not equal to itself, since the map, same as other non-primitive types in JS are compared by reference and not by value. So this will cause the function inside the useEffect to be run on every re-render.
You'd need to wrap it into some kind of deep compare function: https://stackoverflow.com/a/54096391/4468021

Related

How do I make a list updates only on the second render?

I have a state list that is called 'journal' and I want to add a state object that is called 'record' to the list after the user enters the data and set the state of the record.
Here's my states:
const [journal, setJournal] = useState([]);
const [record, setRecord] = useState({});
And here's the method that takes the data from user to set the record:
function AddRecord(debitAccount, debitValue, creditAccount, creditValue, description){
setRecord({date: new Date().getDate().toString(), debit: {[debitAccount]: debitValue},
credit: {[creditAccount]: creditValue}, description, id: new Date().getTime().toString()});
}
I'm using a useEffect to update the journal every time the record changes like this:
useEffect(()=>{
setJournal([...journal, record])
}, [record])
But it adds an empty object at the beginning of the array.
Can someone please tell me how to fix this, I'm still trying to figure my way around states in react, and they're just getting complicated
This is a misuse of useEffect. Effects should be used to react to, and tie together, things which happen outside the business logic of the component (i.e. prop changes, multiple concurrent fetch calls), or resubscribe listeners which are dependent on state values. Just move all the relevant code into AddRecord:
function AddRecord(debitAccount, debitValue, creditAccount, creditValue, description){
const newRecord = {
date: new Date().getDate().toString(),
debit: {
[debitAccount]: debitValue
},
credit: {
[creditAccount]: creditValue
},
description,
id: new Date().getTime().toString()
};
setRecord(newRecord);
setJournal([...journal, newRecord]);
}
useEffect will be called on component initialization try to use some other way maybe useCallback function
useEffect runs on mount, and each time record changes, that's why it runs when record is empty. Just add a condition:
Also do not use journal directly, it's not guarantee that it has the expected value. Use set state callback instead:
useEffect(() => {
if (record && record.id) {
setJournal(journals => ([...journals, record]))
}
}, [record])

useEffect not triggering when object property in dependence array

I have a context/provider that has a websocket as a state variable. Once the socket is initialized, the onMessage callback is set. The callback is something as follows:
const wsOnMessage = (message: any) => {
const data = JSON.parse(message.data);
setProgress(merge(progress, data.progress));
};
Then in the component I have something like this:
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress[pvc.metadata.uid]])
return (
{/* stuff */}
);
}
However, the effect isn't triggering when the progress variable gets updated.
The data structure of the progress variable is something like
{
"uid-here": 0.25,
"another-uid-here": 0.72,
...etc,
}
How can I get the useEffect to trigger when the property that matches pvc.metadata.uid gets updated?
Or, how can I get the component to re-render when that value gets updated?
Quoting the docs:
The function passed to useEffect will run after the render is
committed to the screen.
And that's the key part (that many seem to miss): one uses dependency list supplied to useEffect to limit its invokations, but not to set up some conditions extra to that 'after the render is committed'.
In other words, if your component is not considered updated by React, useEffect hooks just won't be called!
Now, it's not clear from your question how exactly your context (progress) looks like, but this line:
setProgress(merge(progress, data.progress));
... is highly suspicious.
See, for React to track the change in object the reference of this object should change. Now, there's a big chance setProgress just assignes value (passed as its parameter) to a variable, and doesn't do any cloning, shallow or deep.
Yet if merge in your code is similar to lodash.merge (and, again, there's a huge chance it actually is lodash.merge; JS ecosystem is not that big these days), it doesn't return a new object; instead it reassigns values from data.progress to progress and returns the latter.
It's pretty easy to check: replace the aforementioned line with...
setProgress({ ...merge(progress, data.progress) });
Now, in this case a new object will be created and its value will be passed to setProgress. I strongly suggest moving this cloning inside setProgress though; sure, you can do some checks there whether or not you should actually force value update, but even without those checks it should be performant enough.
There seems to be no problem... are you sure pvc.metadata.uid key is in the progress object?
another point: move that dependency into a separate variable after that, put it in the dependency array.
Spread operator create a new reference, so it will trigger the render
let updated = {...property};
updated[propertyname] =value;
setProperty(()=>updated);
If you use only the below code snippet, it will not re-render
let updated = property; //here property is the base object
updated[propertyname] = value;
setProperty(()=>updated);
Try [progress['pvc.metadata.uid']]
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress['pvc.metadata.uid']])
return (
{/* stuff */}
);
}

React useEffect forces me to add dependencies that trigger an infinite loop

Why is React forcing me with their linter plugin to add dependencies that I don't want?
For example, I want my effect to trigger only when a certan value changes, yet the linter tells me to add even functions to the dependencies, and I don't want that.
Why it forces me to do that? What do I gain from that?
/**
* Gets all items, pages, until 250th.
*/
useEffect(() => {
let mounted = true;
if (loadUntil250th && !paginationProps.complete) {
mounted && setLoading(true);
let limit = 250 - paginationProps.page * BATCH_LIMIT;
fetchListItems(paginationProps, limit, paginationProps.page * BATCH_LIMIT)
.then((results) => {
if (mounted) {
setPaginationProps({
...paginationProps,
page: 250 / BATCH_LIMIT,
autoLoad: false,
complete: paginationProps.totalItems <= 250,
});
setListItems(results.listItems);
setLoading(false);
}
})
.catch((err) => {
logger.log('LOADMORE FAILED:', err);
mounted && setPaginationProps({ ...paginationProps, complete: true });
mounted && setLoading(false);
});
}
return () => {
mounted = false;
};
}, [loadUntil250th]);
It wants this array of dependencies, which result in a infinite loop
[loadUntil250th, logger, paginationProps, setListItems]);
I want to understand why it is required, if I don't want them.
The 'exhaustive-deps' lint rule is designed to protect against stale closures, where useEffect references props or state used in the callback but not present in the dependency array. Since logger, paginationProps, and setListItems can theoretically change between renders, it's not safe to reference them inside useEffect without also including them in the dependency array to ensure you're always receiving and acting on up-to-date data. You can think of useEffect as essentially generating a snapshot of all state and props when it gets created and only updating that if one of its dependencies changes.
For instance, without including paginationProps in the dependencies list, if fetchListItems ever modifies the value of paginationProps then useEffect won't have access to that updated value until loadUntil250th changes.
As referenced in this answer, part of the issue is that your usage of useEffect() is unidiomatic. If all you're doing is subscribing to changes to loadUntil250th, you'd be better off moving this function elsewhere and calling it with your code that modifies loadUntil250th.
If you want to keep your code in the useEffect hook, you have a few options:
Assuming that paginationProps and setPaginationProps are derived from a useState hook, you can eliminate the dependency on paginationProps by passing a function to setPaginationProps instead of an object. So your code would become:
setPaginationProps(paginationProps => {
...paginationProps,
page: 250 / BATCH_LIMIT,
autoLoad: false,
complete: paginationProps.totalItems <= 250,
});
Move setListItems inside the useEffect hook if possible. This ensures that you can control whatever props/state that function depends on. If that's not possible, you have a few options. You can move the function outside the component altogether to guarantee that it doesn't depend on props or state. Alternatively, you can use the useCallback hook along with the function to control its dependencies and then list setListItems as another dependency.
It's unlikely that the logger function changes between renders, so you're probably safe keeping that inside your dependency array (although it's odd that the linter would expect that).
If you're still curious, this article is helpful at detailing how useEffect and the dependency array actually works.

Can I use dynamic properties in dependency array for useEffect?

So I have a question regarding useEffect dependenices
This is from the react docs:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
What does this mean exactly, does React keep track of the count variable and its value, and reacts when the value changes, or does React keep track of the first element in the array and its value.
What do I mean by this? Let me explain more. So if we have something like this [name] as dependencies. At the moment of evaluation, the array might result with ['Bob'] or ['Steve']. Clearly this is a change and the useEffect will rerender the component. But how does it check it?
Does it keep track of name or does it keep track of dependencyArray[0]. If we take a look in the previous example, both of these would result to true, name and the first element both changed their values from 'Bob' to 'Steve'. But how does it actually work?
Currently in my code I am using something like this [employees[selectedEmployee].name], where selectedEmployee is something clickable on the UI and it becomes 'Bob' or 'Steve'
ex:
const employees = {
Bob: {
name: 'Bob'
},
Steve: {
name: 'Steve'
}
}
This means that in the end, when evaluated, the dependency array will still result with ['Bob'] --> ['Steve'], and if React is evaluating the dependencyArray[0] then that has clearly changed and component should rerender, but If it keeps track of the reference, then I am changing the reference altogether and it may cause problems.
So what's the correct approach? Can I use dynamic properties like employees[selectedEmployee].name as a dependency?
count is a value, not a reference.
It's just good old Javascript, nothing fancy:
const myArray = [ count ]; // new array containing the value of variable 'count'
const myFunction = () => {
document.title = `You clicked ${count} times`;
}
useEffect(
myFunction,
myArray
);
// Means actually:
// "Run this function if any value in the array
// is different to what it was last time this useEffect() was called"
does React keep track of the ... value, or ... the reference ?
React doesn't really 'keep track' of any of them. It only checks the difference to a previous call, and forgets about everything else.
Can I use dynamic properties as a dependency?
Yes, you can (because they are not as 'dynamic' as you think).
So what's the correct approach?
Better think less of any react-magic going on, but
understand that the component is a function, and believe React calls it when necessary and
think about the variables (properties and state) used inside it, from a plain Javascript perspective.
Then your 'dynamic properties' become 'constant variables during one function call'. No matter which variables change dynamically and how, it will always be one value last time and one value now.
Explaination:
The important 'trick' here is, that the component is just a javascript function, that is called like 'whenever anything might have changed', and consequently useEffect() is also called (as useEffect() is just a function call inside the component).
Only the callback function passed to useEffect is not always called.
useEffect does not render the component, useEffect is called when the component is called, and then just calls the function given to it, or not, depending on if any value in the dependencies array is different to what it was last time useEffect() was called.
React might rerender the component if in the function given to useEffect there are any changes made to the state or something (anything that makes React to think it has to rerender), but that's as a result of this state change, where ever it came from, not because of the useEffect call.
Example:
const MyComponent = (props) => {
// I'm assigning many const here to show we are dealing with local constants.
// Usually you would use this form (using array destructuring):
// const [ selectedEmployee, setSelectedEmployee ] = useState( someInitialValue );
const myStateValueAndSetter = useState( 'Bob' );
const selectedEmployee = myStateValueAndSetter[0];
const setSelectedEmployee = myStateValueAndSetter[1];
const employees = {
Bob: { name: 'Bob' },
Steve: { name: 'Steve' }
};
const currentName = employees[ selectedEmployee ].name;
useEffect(() => {
document.title = 'current name: ' + currentName;
}, [ currentName ]);
return <MyClickableComponent onClick={( newValue ) => {
setSelectedEmployee( newValue )
}}>;
};
click on MyClickableComponent calls the current setSelectedEmployee( newValue ) function.
(The constant selectedEmployee is not changed!)
MyComponent() is called again.
(This is a new function call. All the constants are gone! Only React stores some state in the background.)
useState() is called, the result is stored in a new constant selectedEmployee.
useEffect() is called, and decides if its callback should be called, depending on the previous and the current value of selectedEmployee.
If the callback is not called and nothing else is changed, you might not notice that anything has happened at all.
<MyClickableComponent ... /> is rendered.

Is there a correct way to use an object as a dependency in useEffect?

I currently have a react hook that I'm creating that accepts a query object.
export function useMyQuery(query: QueryObjectType)
{
React.useEffect(executeQuery, [ query ]);
// ...
}
Unfortunately, any time my hook gets called as part of a re-render, despite query having never changed and still being the exact same object as before, I end up with an infinite loop.
I can resolve this by wrapping query with JSON.stringify(...), however I'm not sure if that's correct? Is there any preferred mechanism for testing equality for objects when being passed as a dependency to useEffect?
Adding a useState() should help. You would need to return the setter to allow re-querying from the parent.
useState(query) only fires on the first render, so _query remains the same until explicitly set.
export function useMyQuery(query: QueryObjectType)
{
const [_query, setQuery] = useState(query);
React.useEffect(executeQuery, [ _query ]);
// ...
return setQuery;
}

Resources