276

Lets say I have some state that is dependent on some other state (eg when A changes I want B to change).

Is it appropriate to create a hook that observes A and sets B inside the useEffect hook?

Will the effects cascade such that, when I click the button, the first effect will fire, causing b to change, causing the second effect to fire, before the next render? Are there any performance downsides to structuring code like this?

let MyComponent = props => {
  let [a, setA] = useState(1)
  let [b, setB] = useState(2)
  useEffect(
    () => {
      if (/*some stuff is true*/) {
        setB(3)
      }
    },
    [a],
  )
  useEffect(
    () => {
      // do some stuff
    },
    [b],
  )

  return (
    <button
      onClick={() => {
        setA(5)
      }}
    >
      click me
    </button>
  )
}
Bogdan D
  • 5,321
  • 2
  • 31
  • 32
Dan Ruswick
  • 2,990
  • 2
  • 12
  • 14

6 Answers6

227

Generally speaking, using setState inside useEffect will create an infinite loop that most likely you don't want to cause. There are a couple of exceptions to that rule which I will get into later.

useEffect is called after each render and when setState is used inside of it, it will cause the component to re-render which will call useEffect and so on and so on.

One of the popular cases that using useState inside of useEffect will not cause an infinite loop is when you pass an empty array as a second argument to useEffect like useEffect(() => {....}, []) which means that the effect function should be called once: after the first mount/render only. This is used widely when you're doing data fetching in a component and you want to save the request data in the component's state.

Hossam Mourad
  • 4,369
  • 4
  • 26
  • 22
  • 5
    Another case to use `setState` inside `useEffect` is `setting state` inside subscription or event listeners. But don't forget to cancel the subscription https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup – timqian Apr 29 '19 at 07:48
  • 86
    This is not strictly true - useState only fires if the the value you are updating the state with is different to the previous one so an infinite loop is prevented unless the value changes between cycles. – RobM Jul 04 '19 at 17:17
  • 31
    This answer is incorrect and not to the point of the question: in the code in case, the component renders only twice when the button is clicked, there's no infinite loop – Bogdan D Jul 23 '19 at 15:52
  • 37
    It's a very common use case to set state inside useEffect. Think about data loading, useEffect calls API, gets data, sets using set part of useState. – badbod99 Aug 06 '19 at 11:20
  • 4
    The first paragraph of this answer states unconditionally that this should not be done, but this is very commonly done. Maybe rearrange to make the condition you add in the third paragraph more prominent. – nil Oct 24 '19 at 15:21
  • 5
    I updated the answer to address the issues you mentioned, is it better now? – Hossam Mourad Nov 13 '19 at 07:06
  • 2
    It's worth noting the React team now specifically tells you to do state updates (when component A sets a state using a setter provided by component B) inside a `useEffect` hook, and if you don't you'll get a warning in the latest version of React. – machineghost Mar 23 '20 at 22:38
  • 3
    @machineghost Can you find a link to where the React team says that? – Noumenon Feb 13 '21 at 23:53
  • When I tried to set state in the component body, I got an error message (from React). When I saw that message (nearly a year ago) I *think* it linked to a page, but I don't have the URL handy unfortunately. But really, it just comes down to understanding React: if you set state inside a component's function, it could trigger an infinite loop, because state changes trigger re-renders, so ... you set state, it re-renders, it sets the state again, re-renders again ... `useEffect` avoids this by only running the state-setter when the component is first mounted. – machineghost Feb 14 '21 at 00:32
  • When I try to update state in my `useEffect`, the state variable is not updated. Based on other comments, my call looks like this `setSizeGuideData(sizeGuideData => ({ ...sizeGuideData, getFilteredSizeGuide }));` When I log `sizeGuideData`, I get `[]` returned. `getFilteredSizeGuide` has the correct data. So no idea why it's not working. – Mike S. Aug 03 '21 at 15:12
  • @HossamMourad Since the question clearly has specified dependency array for the `useEffect(callback, [a])`, I think you could update your answer to include the point where useEffect runs **ONCE** after initial rendering and after every rendering **ONLY IF** the dependencies change, `a` and `b` as is in the question. – Dorji Tshering Jul 08 '22 at 13:29
166

For future purposes, this may help too:

It's ok to use setState in useEffect you just need to have attention as described already to not create a loop.

But it's not the only problem that may occur. See below:

Imagine that you have a component Comp that receives props from parent and according to a props change you want to set Comp's state. For some reason, you need to change for each prop in a different useEffect:

DO NOT DO THIS

useEffect(() => {
  setState({ ...state, a: props.a });
}, [props.a]);

useEffect(() => {
  setState({ ...state, b: props.b });
}, [props.b]);

It may never change the state of a as you can see in this example: https://codesandbox.io/s/confident-lederberg-dtx7w

The reason why this happen in this example it's because both useEffects run in the same react cycle when you change both prop.a and prop.b so the value of {...state} when you do setState are exactly the same in both useEffect because they are in the same context. When you run the second setState it will replace the first setState.

DO THIS INSTEAD

The solution for this problem is basically call setState like this:

useEffect(() => {
  setState(state => ({ ...state, a: props.a }));
}, [props.a]);

useEffect(() => {
  setState(state => ({ ...state, b: props.b }));
}, [props.b]);

Check the solution here: https://codesandbox.io/s/mutable-surf-nynlx

Now, you always receive the most updated and correct value of the state when you proceed with the setState.

starball
  • 20,030
  • 7
  • 43
  • 238
Eduardo Pinheiro
  • 3,409
  • 3
  • 30
  • 39
  • 6
    above solution helped me `setName(name => ({ ...name, a: props.a }));` – mufaddal_mw Apr 14 '20 at 09:29
  • 6
    this helped me as well in the part with the arrow function `setItems(items => [...items, item])` – Stefan Zhelyazkov Jun 09 '20 at 16:00
  • 2
    Spent 7 AM to 3 PM without having a solution and now you saved me. – Dinindu Kanchana Sep 19 '20 at 09:24
  • I tried this solution. `setState(state => ({ ...state, useEffectValue }));` When I log `state`, it remains empty. If I pass `useEffectValue` in the output array, I get an infinite loop. ¯\_(ツ)_/¯ – Mike S. Aug 03 '21 at 15:05
  • can't thank you enough for this incredible tip. I was indeed overriding the state from multiple useEffect calls that were triggering all when mounting the component. – JSancho Aug 11 '21 at 15:45
  • The above solution works. Directly trying change the state didn't help. However, passing in an updater function did the trick. reference: https://reactjs.org/docs/faq-state.html – dodobird Feb 01 '22 at 04:28
  • I had exact same issue. I stacked without any idea what happening thanks for greate explanation. – Hyzyr Feb 17 '22 at 08:23
  • 1
    Should probably be a linting rule – Dan Mandel Oct 24 '22 at 06:34
42

Effects are always executed after the render phase is completed even if you setState inside the one effect, another effect will read the updated state and take action on it only after the render phase.

Having said that its probably better to take both actions in the same effect unless there is a possibility that b can change due to reasons other than changing a in which case too you would want to execute the same logic

vsync
  • 118,978
  • 58
  • 307
  • 400
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • 9
    So if A changes B, the component would render twice right? – Dan Ruswick Dec 11 '18 at 15:22
  • 2
    @alaboudi Yes, if A changes causing useeffect to run which sets B then the component does render twice – Shubham Khatri Jul 17 '20 at 04:56
  • @alaboudi Yes.. as Shubham Khatri said it will render again. but you can skip calling your effect after the re-rendering using the second argument refer https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects – Karthikeyan Dec 28 '20 at 08:07
37

useEffect can hook on a certain prop or state. so, the thing you need to do to avoid infinite loop hook is binding some variable or state to effect

For Example:

useEffect(myeffectCallback, [])

above effect will fire only once the component has rendered. this is similar to componentDidMount lifecycle

const [something, setSomething] = withState(0)
const [myState, setMyState] = withState(0)
useEffect(() => {
  setSomething(0)
}, myState)

above effect will fire only my state has changed this is similar to componentDidUpdate except not every changing state will fire it.

You can read more detail though this link

Mosia Thabo
  • 4,009
  • 1
  • 14
  • 24
Dogies007
  • 381
  • 3
  • 4
  • 2
    Thank you, this answer addresses the dependency array of useEffect in a way other answer did not. Including an empty array as a second argument to useEffect will make sure useEffect executes once the component has rendered, but including an array with a specific state or specific states will cause the useEffect to execute when the states in reference have changed. – adriaanbd Oct 20 '20 at 03:30
  • 10
    I don't understand what `withState()` represents. I can't find any reference to it in the doc. – Mike S. Aug 03 '21 at 15:07
29

▶ 1. Can I set state inside a useEffect hook?

In principle, you can set state freely where you need it - including inside useEffect and even during rendering. Just make sure to avoid infinite loops by settting Hook deps properly and/or state conditionally.


▶ 2. Lets say I have some state that is dependent on some other state. Is it appropriate to create a hook that observes A and sets B inside the useEffect hook?

You just described the classic use case for useReducer:

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. (React docs)

When setting a state variable depends on the current value of another state variable, you might want to try replacing them both with useReducer. [...] When you find yourself writing setSomething(something => ...), it’s a good time to consider using a reducer instead. (Dan Abramov, Overreacted blog)

let MyComponent = () => {
  let [state, dispatch] = useReducer(reducer, { a: 1, b: 2 });

  useEffect(() => {
    console.log("Some effect with B");
  }, [state.b]);

  return (
    <div>
      <p>A: {state.a}, B: {state.b}</p>
      <button onClick={() => dispatch({ type: "SET_A", payload: 5 })}>
        Set A to 5 and Check B
      </button>
      <button onClick={() => dispatch({ type: "INCREMENT_B" })}>
        Increment B
      </button>
    </div>
  );
};

// B depends on A. If B >= A, then reset B to 1.
function reducer(state, { type, payload }) {
  const someCondition = state.b >= state.a;

  if (type === "SET_A")
    return someCondition ? { a: payload, b: 1 } : { ...state, a: payload };
  else if (type === "INCREMENT_B") return { ...state, b: state.b + 1 };
  return state;
}

ReactDOM.render(<MyComponent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect } = React</script>

▶ 3. Will the effects cascade such that, when I click the button, the first effect will fire, causing b to change, causing the second effect to fire, before the next render?

useEffect always runs after the render is committed and DOM changes are applied. The first effect fires, changes b and causes a re-render. After this render has completed, second effect will run due to b changes.

let MyComponent = props => {
  console.log("render");
  let [a, setA] = useState(1);
  let [b, setB] = useState(2);

  let isFirstRender = useRef(true);

  useEffect(() => {
    console.log("useEffect a, value:", a);
    if (isFirstRender.current) isFirstRender.current = false;
    else setB(3);
    return () => {
      console.log("unmount useEffect a, value:", a);
    };
  }, [a]);
  useEffect(() => {
    console.log("useEffect b, value:", b);
    return () => {
      console.log("unmount useEffect b, value:", b);
    };
  }, [b]);

  return (
    <div>
      <p>a: {a}, b: {b}</p>
      <button
        onClick={() => {
          console.log("Clicked!");
          setA(5);
        }}
      >
        click me
      </button>
    </div>
  );
};

ReactDOM.render(<MyComponent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>

▶ 4. Are there any performance downsides to structuring code like this?

Yes. By wrapping the state change of b in a separate useEffect for a, the browser has an additional layout/paint phase - these effects are potentially visible for the user. If there is no way you want give useReducer a try, you could change b state together with a directly:

let MyComponent = () => {
  console.log("render");
  let [a, setA] = useState(1);
  let [b, setB] = useState(2);

  useEffect(() => {
    console.log("useEffect b, value:", b);
    return () => {
      console.log("unmount useEffect b, value:", b);
    };
  }, [b]);

  const handleClick = () => {
    console.log("Clicked!");
    setA(5);
    b >= 5 ? setB(1) : setB(b + 1);
  };

  return (
    <div>
      <p>
        a: {a}, b: {b}
      </p>
      <button onClick={handleClick}>click me</button>
    </div>
  );
};

ReactDOM.render(<MyComponent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
ford04
  • 66,267
  • 20
  • 199
  • 171
2

Try wrapping the setState inside an if-statement that checks whether the state needs to be changed - if yes, change it, else return () => {}

e.g.,

useEffect(() => {
    if(a.currentCondition !== a.desiredCondition) {
        setA();
    }
    return cleanup;
}, [b])