Reputation: 155
Given a functional React component with a local function fn()
and numeric counters a
and b
:
const [a, setA] = useState(0);
const [b, setB] = useState(0);
function fn() { console.log(a, b); }
Where the function fn()
is called when a
or b
change:
useEffect(()=> {
fn();
}, [a, b]);
We'll have two buttons that increment a
and b
by 1.
<button onClick={()=> setA(a => a + 1)}>Increment A: {a}</button>
<button onClick={()=> setB(b => b + 1)}>Increment B: {b}</button>
but when a
increments, we'll increment b
as well:
useEffect(()=> {
setB(b => b + 1);
}, [a]);
How do I make the fn()
run only once when clicking on "Increment A" and not react to both changes individually? The rendering pass that updates a
state variable causes b
to change, which will cause another render pass. How can I prevent this and make it happen in one pass?
Edit:
Addition #1: b
doesn't have to change always, so let's say that this useEffect
resets b
to 0.
useEffect(()=> {
setB(0);
}, [a]);
Addition #2: Let's change a
not to be a local state variable, but an input prop, which changes outside of the scope of this component.
Upvotes: 1
Views: 482
Reputation: 370619
If, when a
always changes, b
changes too, then it's simple - just change the dependency array to be only [b]
. If the first changes comes from a
, fn
will run after the [b]
effect. If the first change comes from b
, fn
will run just after the first re-render.
useEffect(()=> {
fn();
}, [b]);
Another approach that can reduce rerenderings in similar situations would be to exchange setA
with another function that both sets a
and runs setB
. Instead of
const [a, setA] = useState(0);
const [b, setB] = useState(0);
useEffect(()=> {
setB(b => b + 1);
}, [a]);
you can have
const [a, setAUnbound] = useState(0);
const [b, setB] = useState(0);
const setA = (newA) => {
setAUnbound(newA);
setB(b => b + 1);
};
and not reference setAUnbound
again. This way, the functionality of what was previously in the effect is now baked into the state setter function. (I usually like this sort of approach more than using useEffect
. It not only makes things feel a bit cleaner, it also prevents linters from complaining about exhaustive-deps
)
Now you can call setA
like you would any state setter without a callback.
<button onClick={()=> setA(a + 1)}>Increment A: {a}</button>
Using callbacks to set state isn't necessary here, but if you were in a situation where it was needed, you can check the typeof
the newA
argument to determine whether you need to call it or not.
If the situation is different and
Let's say I on every change of
a
I would setb
to 0. That would require botha
andb
to be in the dependency array. Alsoa
could be an input prop and change outside of this component.
then one way to manage this would be to move the child's state into the parent and have the parent update everything at once (either by combining the two state variables into one, or by using the setAUnbound
approach described above). Then the child component would only see one change and its effect would run just once.
Messier approaches exist, but the above are what I'd prefer.
Upvotes: 1