You can pass JSON.stringify(outcomes) as the dependency list:
Read more here
useEffect(() => {
console.log(outcomes)
}, [JSON.stringify(outcomes)])
Answer from Loi Nguyen Huynh on Stack OverflowYou can pass JSON.stringify(outcomes) as the dependency list:
Read more here
useEffect(() => {
console.log(outcomes)
}, [JSON.stringify(outcomes)])
Using JSON.stringify() or any deep comparison methods may be inefficient, if you know ahead the shape of the object, you can write your own effect hook that triggers the callback based on the result of your custom equality function.
useEffect works by checking if each value in the dependency array is the same instance with the one in the previous render and executes the callback if one of them is not. So we just need to keep the instance of the data we're interested in using useRef and only assign a new one if the custom equality check return false to trigger the effect.
function arrayEqual(a1: any[], a2: any[]) {
if (a1.length !== a2.length) return false;
for (let i = 0; i < a1.length; i++) {
if (a1[i] !== a2[i]) {
return false;
}
}
return true;
}
type MaybeCleanUpFn = void | (() => void);
function useNumberArrayEffect(cb: () => MaybeCleanUpFn, deps: number[]) {
const ref = useRef<number[]>(deps);
if (!arrayEqual(deps, ref.current)) {
ref.current = deps;
}
useEffect(cb, [ref.current]);
}
Usage
function Child({ arr }: { arr: number[] }) {
useNumberArrayEffect(() => {
console.log("run effect", JSON.stringify(arr));
}, arr);
return <pre>{JSON.stringify(arr)}</pre>;
}
Taking one step further, we can also reuse the hook by creating an effect hook that accepts a custom equality function.
type MaybeCleanUpFn = void | (() => void);
type EqualityFn = (a: DependencyList, b: DependencyList) => boolean;
function useCustomEffect(
cb: () => MaybeCleanUpFn,
deps: DependencyList,
equal?: EqualityFn
) {
const ref = useRef<DependencyList>(deps);
if (!equal || !equal(deps, ref.current)) {
ref.current = deps;
}
useEffect(cb, [ref.current]);
}
Usage
useCustomEffect(
() => {
console.log("run custom effect", JSON.stringify(arr));
},
[arr],
(a, b) => arrayEqual(a[0], b[0])
);
Live Demo
Passing an array as dependency for useEffect in React
reactjs - How to manage dependency arrays with useEffect - Stack Overflow
You can't use objects or arrays in useEffect?
reactjs - Passing a function in the useEffect dependency array causes infinite loop - Stack Overflow
Why is the dependency array important in useEffect?
What happens if you don’t pass a dependency array to useEffect?
Can you use objects or arrays in the useEffect dependency array?
Videos
I'm having an issue with useEffect not firing if an array in the dependency list changes.
//props.shapes = ['stuff', 'in', 'the', 'array']
useEffect(() => {
// do stuff
}, [props.shapes])Nothing in that useEffect runs. This...should work right? If the array changes higher up in the component tree a new array is generated and that should trigger a re-render in this component and then the useEffect will fire, right?
What's weird is if I make it dependent on the length, it does work
//props.shapes = ['stuff', 'in', 'the', 'array']
useEffect(() => {
// do stuff
}, [props.shapes.length])My gut tells me that this array is actually getting mutated somewhere and that is causing the issue...but I don't see that in the app. What am I fundamentally misunderstanding about arrays (and I guess objects) with useEffect?
Ninja Edit: I know that using .length in the array dependency will lead to bugs if the array values change but the length doesn't. I don't plan on using this, just explaining what I'm experiencing.
Your gut is correct, as far as I can tell. How are you updating the array? Some context would be helpful if possible.
If you're using push/pop/mutating a value in the array, it will be referentially equal (oldArray === mutatedArray) and the effect will not be rerun.
It looks like the second argument in your useEffect call has a square opening bracket like an array, but no closing square bracket.
One of the rules of React hooks is that useEffect must include all variables that it references in its dependency array. That's why you're getting the error. You can have additional variables, like varB, but you cannot exclude one, otherwise you could run into some wonky behavior.
A way around it is to use a ref, which allows you to store the value in a way that never triggers a render. The ref will need to be included in the dependency array, but since it never changes it won't trigger the useEffect again.
const varARef = useRef(varA)
useEffect(()=> {
DO SOMETHING TO varARef.current;
}, [varARef, varB])
Note that if you do want varARef.current to stay in sync with varA, you'll want to add a line for that:
const varARef = useRef(varA)
varARef.current = varA
useEffect(()=> {
DO SOMETHING TO varARef.current;
}, [varARef, varB])
If you have to update the state of a variable using its current state the best way to do it is using a callback inside setState.
If you change your code like this no issue will be triggered.
[varB, setVarB] = useState("");
[counter, setCounter] = useState(0);
useEffect(()=>{
setCounter(currentCounter => currentCounter+1);
},[varB]) // No warning here
Full answer: useEffect has a missing dependency when updating state based on current value
Specifically, inside the dependency array. I've scoured the docs to find an answer to this question but couldn't find anything. Even read Dan Abramov's long guide to useEffect and still couldn't figure it out.
Is it true that, since 1) rerenders recreate objects/arrays that are declared inside your component 2) the dependency array compares objects by reference, not by value, that if you put an object inside a useEffect dependency array it'll run every time?
If so, what about if I memoize the object with useMemo? Also, what about state variables? Does React check a useState value differently than if you declare something inside the component?
If anyone can help me understand this better, even if it's just linking to a specific part of the docs, I'd appreciate it a lot.
The issue is that upon each render cycle, markup is redefined. React uses shallow object comparison to determine if a value updated or not. Each render cycle markup has a different reference. You can use useCallback to memoize the function though so the reference is stable. Do you have the react hook rules enabled for your linter? If you did then it would likely flag it, tell you why, and make this suggestion to resolve the reference issue.
const markup = useCallback(
(count) => {
const stringCountCorrection = count + 1;
return (
// Some markup that references the sections prop
);
},
[/* any dependencies the react linter suggests */]
);
// No infinite looping, markup reference is stable/memoized
useEffect(() => {
if (sections.length) {
const sectionsWithMarkup = sections.map((section, index) => markup(index));
setSectionBlocks(blocks => [...blocks, ...sectionsWithMarkup]);
} else {
setSectionBlocks([]);
}
}, [sections, markup]);
Alternatively if the markup function is only used in the useEffect hook you can move it directly into the hook callback to remove it as an external dependency for the hook.
Example:
useEffect(() => {
const markup = (count) => {
const stringCountCorrection = count + 1;
return (
// Some markup that references the sections prop
);
};
if (sections.length) {
const sectionsWithMarkup = sections.map((section, index) => markup(index));
setSectionBlocks(blocks => [...blocks, ...sectionsWithMarkup]);
} else {
setSectionBlocks([]);
}
}, [sections, /* any other dependencies the react linter suggests */]);
Additionally, if the markup function has absolutely no external dependencies, i.e. it is a pure function, then it could/should be declared outside any React component.
Why is an infinite loop created when I pass a function expression
The "infinite loop" is the component re-rendering over and over because the markup function is a NEW function reference (pointer in memory) each time the component renders and useEffect triggers the re-render because it's a dependency.
The solution is as @drew-reese pointed out, use the useCallback hook to define your markup function.