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
javascript - How to use useEffect hook with the dependency list as a specific field in an array of objects? - Stack Overflow
reactjs - useEffect and dependency array - Stack Overflow
reactjs - Passing a function in the useEffect dependency array causes infinite loop - Stack Overflow
You can't use objects or arrays in useEffect?
Can you use objects or arrays in the useEffect dependency array?
Why is the dependency array important in useEffect?
What happens if you don’t pass a dependency array to useEffect?
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.
Just map the countries into the effect dependency array.
const countries = data.map((x) => x.country);
useEffect(() => {
console.log(countries);
}, countries);
Normally you wouldn't want to do that, but just to answer your question, it can be done, so let me propose the following assuming your list is called items:
useEffect(() => {
}, [...items.map(v => v.country)])
What the above code does is to spread all items (with its country property) into the useEffect dependency array.
The reason why this can be adhoc is mainly because React doesn't like to have a variable length of dependency. In the source code, when the length changes, it only appreciates the element change from the existing elements. So you might run into problem if you switch from 1 elements to 2 elements.
However if you have fixed number of elements, this should do what you wanted. Keep in mind the items has to be an array at all time.
NOTE: to accommodate the length issue, maybe we can add an additional variable length to the dependency array :)
}, [items.length, ...items.map(v => v.country)])
As i mentioned, most of time, you should avoid doing this, instead try to change the entire items every time when an item changes. And let the Item display to optimize, such as React.memo.
useEffect will re-run with an object in it's dependency array whenever that object changes references.
For example in your code, user is always stable until you call setUser in which case it will change to a new reference and run at that point.
If you instead defined user as a new object in each render, then the useEffect would instead run every time the component re-rendered.
const user = {
firstName: 'Joe',
lastName: 'Smith',
address: {
home: '123 street',
},
};
It's all about referential equality. Whether previousUser===newUser on each render. React essentially performs a check very similar to that (I believe Object.is) for every variable in the dependency array.
Bellow your component.
changes:
- move
initialStateoutside UseEffectHook. You don't need to createinitialStateevery re-render; - use useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed;
setUserandsetCountwith updater function;handleOnChangeUserable to use for all fields of user object. Every time handleOnChangeUser - create new ref to user object. in this case useEffect able to detect changes;
import React, { useCallback, useEffect, useState } from 'react';
const initialState = {
firstName: "Joe",
lastName: "Smith",
address: {
home: "123 street",
}
};
export function UseEffectHook() {
const [user, setUser] = useState(initialState);
const [count, setCount] = useState(0);
const handleOnChangeUser = useCallback((event) => {
const { name, value } = event;
setUser(prevState => ({ ...prevState, [name]: value }));
}, []);
const increaseCount = useCallback(() => {
setCount(prevState => prevState + 1);
}, []);
useEffect(() => {
console.log("triggered");
}, [user]);
return (
<div>
<p>useEffect - Practice with different deps</p>
{JSON.stringify(user)}
<label>
<span>Firstname:</span>
<input
type="text"
name="firstName"
onChange={handleOnChangeUser}
/>
</label>
<br />
<label>
<span>Lastname:</span>
<input
type="text"
name="lastName"
onChange={handleOnChangeUser}
/>
</label>
<button onClick={increaseCount}>Increase</button>
</div>
);
}
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.
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.