I'm struggling to understand when and why exactly useReducer
has advantages when compared to useState
. There are many arguments out there but to me, none of them makes sense and in this post, I'm trying to apply them to a simple example.
Maybe I am missing something but I don't see why useReducer
should be used anywhere over useState
. I hope you can help me to clarify this.
Let's take this example:
function CounterControls(props) {
return (
<>
<button onClick={props.increment}>increment</button>
<button onClick={props.decrement}>decrement</button>
</>
);
}
export default function App() {
const [complexState, setComplexState] = useState({ nested: { deeply: 1 } });
function increment() {
setComplexState(state => {
// do very complex logic here that depends on previous complexState
state.nested.deeply += 1;
return { ...state };
});
}
function decrement() {
setComplexState(state => {
// do very complex logic here that depends on previous complexState
state.nested.deeply -= 1;
return { ...state };
});
}
return (
<div>
<h1>{complexState.nested.deeply}</h1>
<CounterControls increment={increment} decrement={decrement} />
</div>
);
}
See this stackblitz
import React from "react";
import { useReducer } from "react";
function CounterControls(props) {
return (
<>
<button onClick={() => props.dispatch({ type: "increment" })}>
increment
</button>
<button onClick={() => props.dispatch({ type: "decrement" })}>
decrement
</button>
</>
);
}
export default function App() {
const [complexState, dispatch] = useReducer(reducer, {
nested: { deeply: 1 }
});
function reducer(state, action) {
switch (action.type) {
case "increment":
state.nested.deeply += 1;
return { ...state };
case "decrement":
state.nested.deeply -= 1;
return { ...state };
default:
throw new Error();
}
}
return (
<div>
<h1>{complexState.nested.deeply}</h1>
<CounterControls dispatch={dispatch} />
</div>
);
}
See this stackblitz
In a lot of articles (including the docs) two argumentations seem to be very popular:
"useReducer is good for complex state logic". In our example, let's say complexState
is complex have many modification actions with a lot of logic each. How does useReducer
help here? For complex states wouldn't it be even better to have individual functions instead of having a single 200 lines reducer function?
"useReducer is good if the next state depends on the previous one". I can do the exact same thing with useState, can't I? Simply write setState(oldstate => {...})
Potential other advantages:
Disadvantages I see:
With all that in mind: Can you give me a good example where useReducer
really shines and that can't easily be rewritten to a version with useState
?
Get to know 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. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.
useReducer returns a state and a dispatch function, while receiving a reducer and an initial state. What is a reducer? A reducer is a pure function which has two parameters, a state and an action (now we'll understand the dispatch from useReducer).
Let’s review a few common reasons why people choose useReducer over useState: So business logic can be centralized in the reducer as opposed to scattered about the component Reducers are pure functions that are easy to test in isolation of React
In a lot of articles (including the docs) two argumentations seem to be very popular: "useReducer is good for complex state logic". In our example, let's say complexState is complex have many modification actions with a lot of logic each. How does useReducer help here?
A couple of months later, I feel like I have to add some insights to this topic. If choosing between useReducer
and useState
was just a matter of personal preferences, why would people write stuff like this:
Dan Abramov on twitter:
useReducer is truly the cheat mode of Hooks. You might not appreciate it at first but it avoids a whole lot of potential issues that pop up both in classes and in components relying on useState. Get to know useReducer.
React docs
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. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.
React docs:
We recommend to pass dispatch down in context rather than individual callbacks in props.
So let's try to nail it down and find a scenario, where useReducer
clearly shines over useState
:
VersionA's approach (useState
& pass down callbacks) can have problems with this:
App
!useCallback
on the function helps, but this pattern can quickly become tedious, especially if we need to additionally call useMemo
on an actions
object. (Also I'm no expert on this, but it doesn't sound very convincing from a performance perspective)useCallback
wouldn't help much.If we go with a reducer instead:
dispatch
function always has a stable identity! (See react docs)Again, see Dan Abramov's Twitter Post:
And the “dispatch” identity is always stable, even if the reducer is inline. So you can rely on it for perf optimizations and pass dispatch down the context for free as a static value.
In this code, I try to highlight some of the advantages of working with useReducer
that I tried to describe previously:
import React, { useEffect } from "react";
import { useState, useReducer } from "react";
function MyControls({ dispatch }) {
// Cool, effect won't be called if reducer function changes.
// dispatch is stable!
// And still the up-to-date reducer will be used if we call it
useEffect(() => {
function onResize() {
dispatch({ type: "set", text: "Resize" });
}
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, [dispatch]);
return (
<>
<button onClick={() => dispatch({ type: "set", text: "ABC" })}>
Set to "ABC"
</button>
<button onClick={() => dispatch({ type: "setToGlobalState" })}>
Set to globalAppState
</button>
<div>Resize to set to "Resized"</div>
</>
);
}
function MyComponent(props) {
const [headlineText, dispatch] = useReducer(reducer, "ABC");
function reducer(state, action) {
switch (action.type) {
case "set":
return action.text;
case "setToGlobalState":
// Cool, we can simply access props here. No dependencies
// useCallbacks etc.
return props.globalAppState;
default:
throw new Error();
}
}
return (
<div>
<h1>{headlineText}</h1>
<MyControls dispatch={dispatch} />
</div>
);
}
export default function App() {
const [globalAppState, setGlobalAppState] = useState("");
return (
<div>
global app state:{" "}
<input
value={globalAppState}
onChange={(e) => setGlobalAppState(e.target.value)}
/>
<MyComponent globalAppState={globalAppState} />
</div>
);
}
See this codesandbox
dispatch
's identity stays the same! It doesn't trigger the effectI believe this may end up in an argument of opinions. however, this extraction from a simple article speaks for me so here it is with a link to the whole article at the bottom.
useReducer() is an alternative to useState() which gives you more control over the state management and can make testing easier. All the cases can be done with useState() method, so in conclusion, use the method that you are comfortable with, and it is easier to understand for you and colleagues.
Ref. Article: https://dev.to/spukas/3-reasons-to-usereducer-over-usestate-43ad#:~:text=useReducer()%20is%20an%20alternative,understand%20for%20you%20and%20colleagues.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With