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