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:
Version A - with useState
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
Version B - with useReducer
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
Comparing these two Variants, are there any potential advantages that Version B's approach (going with useReducer
) would have over Version A's approach? At least for me, I don't see a reason to go with useReducer
in a situation like this. But in which scenarios should I?
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 assume complexState
is actually complex and there are 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 with useReducer
:
- "I don't have to pass down multiple functions but only one reducer": Ok, but I could also wrap my functions into one "actions" object with useCallback etc. And as already mentioned, having different logic in different functions seems like a good thing for me.
- "I can provide the reducer with a context so my complex state can easily be modified throughout the app". Yes, but you could just as well provide individual functions from that context (maybe wrapped by useCallback)
Disadvantages I see with useReducer
:
- Multiple different actions in a single super-long function seems confusing
- More prone to errors, since you have to examine the reducer function or rely on typescript etc. to find out what string you can pass on to the reducer and what arguments come with it. When calling a function this is much more straightforward.
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
?