Currently useEffect
is fired when just one of the dependencies have changed.
How could I update it / use it to fire when all of the dependencies have changed?
Currently useEffect
is fired when just one of the dependencies have changed.
How could I update it / use it to fire when all of the dependencies have changed?
You'll need to add some logic to call your effect when all dependencies have changed. Here's useEffectAllDepsChange
that should achieve your desired behavior.
The strategy here is to compare the previous deps with the current. If they aren't all different, we keep the previous deps in a ref an don't update it until they are. This allows you to change the deps multiple times before the the effect is called.
import React, { useEffect, useState, useRef } from "react";
// taken from https://usehooks.com/usePrevious/
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function useEffectAllDepsChange(fn, deps) {
const prevDeps = usePrevious(deps);
const changeTarget = useRef();
useEffect(() => {
// nothing to compare to yet
if (changeTarget.current === undefined) {
changeTarget.current = prevDeps;
}
// we're mounting, so call the callback
if (changeTarget.current === undefined) {
return fn();
}
// make sure every dependency has changed
if (changeTarget.current.every((dep, i) => dep !== deps[i])) {
changeTarget.current = deps;
return fn();
}
}, [fn, prevDeps, deps]);
}
export default function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
useEffectAllDepsChange(() => {
console.log("running effect", [a, b]);
}, [a, b]);
return (
<div>
<button onClick={() => setA((prev) => prev + 1)}>A: {a}</button>
<button onClick={() => setB((prev) => prev + 1)}>B: {b}</button>
</div>
);
}
An alternate approach inspired by Richard is cleaner, but with the downside of more renders across updates.
function useEffectAllDepsChange(fn, deps) {
const [changeTarget, setChangeTarget] = useState(deps);
useEffect(() => {
setChangeTarget(prev => {
if (prev.every((dep, i) => dep !== deps[i])) {
return deps;
}
return prev;
});
}, [deps]);
useEffect(fn, changeTarget);
}
I like @AustinBrunkhorst's soultion, but you can do it with less code.
Use a state object that is only updated when your criteria is met, and set it within a 2nd useEffect.
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [ab, setAB] = useState({a, b});
useEffect(() => {
setAB(prev => {
console.log('prev AB', prev)
return (a !== prev.a && b !== prev.b)
? {a,b}
: prev; // do nothing
})
}, [a, b])
useEffect(() => {
console.log('both have changed')
}, [ab])
return (
<div className="App">
<div>Click on a button to increment its value.</div>
<button onClick={() => setA((prev) => prev + 1)}>A: {a}</button>
<button onClick={() => setB((prev) => prev + 1)}>B: {b}</button>
</div>
);
}
You'll have to track the previous values of your dependencies and check if only one of them changed, or both/all. Basic implementation could look like this:
import React from "react";
const usePrev = value => {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
const App = () => {
const [foo, setFoo] = React.useState(0);
const [bar, setBar] = React.useState(0);
const prevFoo = usePrev(foo);
const prevBar = usePrev(bar);
React.useEffect(() => {
if (prevFoo !== foo && prevBar !== bar) {
console.log("both foo and bar changed!");
}
}, [prevFoo, prevBar, foo, bar]);
return (
<div className="App">
<h2>foo: {foo}</h2>
<h2>bar: {bar}</h2>
<button onClick={() => setFoo(v => v + 1)}>Increment foo</button>
<button onClick={() => setBar(v => v + 1)}>Increment bar</button>
<button
onClick={() => {
setFoo(v => v + 1);
setBar(v => v + 1);
}}
>
Increment both
</button>
</div>
);
};
export default App;
Here is also a CodeSandbox link to play around.
You can check how the usePrev
hook works elsewhere, e.g here.
FWIW, react-use is a nice library of additional hooks for react that has ~30k stars on GitHub:
https://github.com/streamich/react-use
And one of those custom hooks is the useCustomCompareEffect:
https://github.com/streamich/react-use/blob/master/docs/useCustomCompareEffect.md
Which could be easily used to handle this kind of custom comparison
To demonstrate how you can compose hooks in various manners, here's my approach. This one doesn't invoke the effect in the initial attribution.
import React, { useEffect, useRef, useState } from "react";
import "./styles.css";
function usePrevious(state) {
const ref = useRef();
useEffect(() => {
ref.current = state;
});
return ref.current;
}
function useAllChanged(callback, array) {
const previousArray = usePrevious(array);
console.log("useAllChanged", array, previousArray);
if (previousArray === undefined) return;
const allChanged = array.every((state, index) => {
const previous = previousArray[index];
return previous !== state;
});
if (allChanged) {
callback(array, previousArray);
}
}
const randomIncrement = () => Math.floor(Math.random() * 4);
export default function App() {
const [state1, setState1] = useState(0);
const [state2, setState2] = useState(0);
const [state3, setState3] = useState(0);
useAllChanged(
(state, prev) => {
alert("Everything changed!");
console.info(state, prev);
},
[state1, state2, state3]
);
const onClick = () => {
console.info("onClick");
setState1(state => state + randomIncrement());
setState2(state => state + randomIncrement());
setState3(state => state + randomIncrement());
};
return (
<div className="App">
<p>State 1: {state1}</p>
<p>State 2: {state2}</p>
<p>State 3: {state3}</p>
<button onClick={onClick}>Randomly increment</button>
</div>
);
}