score:39

Accepted answer

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

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

score:24

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

score:35

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

score:128

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.

i hope this helps someone!

score:188

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.


Related Query

More Query from same tag