LukasH
LukasH

Reputation: 155

How to prevent useEffect from being called multiple times when there's a relationship between it's dependencies?

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

Answers (1)

CertainPerformance
CertainPerformance

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 set b to 0. That would require both a and b to be in the dependency array. Also a 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

Related Questions