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 array to useEffect dependency list
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.
More on reddit.comreactjs - React useEffect dependency array and implementation - Stack Overflow
reactjs - Passing a function in the useEffect dependency array causes infinite loop - Stack Overflow
reactjs - useEffect and dependency array - Stack Overflow
Why is the dependency array important in useEffect?
What is the useEffect hook used for in React?
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.
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.
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>
);
}
You can make a custom hook to do what you want:
In this example, we replace the last element in the array, and see the output in the console.
import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
import { isEqual } from "lodash";
const usePrevious = value => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
const App = () => {
const [arr, setArr] = useState([2, 4, 5]);
const prevArr = usePrevious(arr);
useEffect(() => {
if (!isEqual(arr, prevArr)) {
console.log(`array changed from ${prevArr} to ${arr}`);
}
}, [prevArr]);
const change = () => {
const temp = [...arr];
temp.pop();
temp.push(6);
setArr(temp);
};
return (
<button onClick={change}>change last array element</button>
)
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Live example here.
Your effect is triggered based on the "shopUsers" prop, which itself triggers a redux action that updates the "shopUsers" prop and thats why it keeps infinitely firing.
I think what you want to optimize is the rendering of your component itself, since you're already using redux, I'm assuming your props/state are immutable, so you can use React.memo to re-render your component only when one of its props change.
Also you should define your state/props variable outside of your hooks since they're used in the scope of the entire function like so.
In your case, if you pass an empty array as a second param to memo, then it will only fire on ComponentDidMount, if you pass null/undefined or dont pass anything, it will be fired on ComponentDidMount + ComponentDidUpdate, if you want to optimise it that even when props change/component updates the hook doesn't fire unless a specific variable changes then you can add some variable as your second argument
React.memo(function(props){
const [isLoading, setLoading] = useState(false);
const { getStoreUsers, shopUsers } = props;
useEffect(() => {
setLoading(true);
getStoreUsers().then(() => {
setLoading(false);
}).catch((err) => {
setLoading(false);
});
}, []);
...
})