298

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>
  )
}

6 Answers 6

246

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.

Sign up to request clarification or add additional context in comments.

11 Comments

Another case to use setState inside useEffect is setting state inside subscription or event listeners. But don't forget to cancel the subscription reactjs.org/docs/hooks-effect.html#effects-with-cleanup
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.
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
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.
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.
|
188

For future purposes, this may help too:

It's ok to use setState in useEffect . To do so, however, you need to ensure you don't unintentionally create an infinite loop.

An infinite loop is 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 this occurs is that both useEffect hooks run during the same react render cycle. When props.a and props.b change at the same time, each useEffect captures the same stale state value from before the update. As a result, when the first effect runs, it calls setState({ ...state, a: props.a }) . Then, the second effect runs immediately after and calls setState({ ...state, b: props.b}) with the same stale state value, thereby overwriting the first update. The result is that a never appears to update. The second setState replaces the first one rather than merging the two updates together.

DO THIS INSTEAD

The solution to this problem is to call setState like this:

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

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

For more information, check the solution here: https://codesandbox.io/s/mutable-surf-nynlx

With this approach, you will always receive the most updated and correct value of the state.

9 Comments

above solution helped me setName(name => ({ ...name, a: props.a }));
For clarity, this works because your state variable is named name, and you're using the spread operator (...) to expand its existing properties into the new object. However, this can be misleading because the values inside of name do NOT represent the "name" you're updating. Hopefully this makes sense.
this helped me as well in the part with the arrow function setItems(items => [...items, item])
Spent 7 AM to 3 PM without having a solution and now you saved me.
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. ¯_(ツ)_/¯
Should probably be a linting rule
|
46

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

3 Comments

So if A changes B, the component would render twice right?
@alaboudi Yes, if A changes causing useeffect to run which sets B then the component does render twice
@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 reactjs.org/docs/…
39

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] = useState(0)
const [myState, setMyState] = useState(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

4 Comments

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.
I don't understand what withState() represents. I can't find any reference to it in the doc.
I think that should be useState(0).
I edited the answer to replace withState with useState
32

▶ 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>

Comments

3

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])

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.