Exploring the similarities and differences between useState and useReducer
In this section, we demonstrate some similarities and differences between useState
and useReducer
.
Implementing useState with useReducer
Implementing useState
with useReducer
instead is 100% possible. Actually, it's known that useState
is implemented with useReducer
inside React.
Important Note
This may not hold in the future as useState
could be implemented more efficiently.
The following example shows how to implement useState
with useReducer
:
const useState = (initialState) => {
const [state, dispatch] = useReducer(
(prev, action) =>
typeof action === 'function' ? action(prev) : action,
initialState
);
return [state, dispatch];
};
This can then be simplified and improved upon, as follows:
const reducer = (prev, action) =>
typeof action === 'function' ? action(prev): prev;
const useState = (initialState) =>
useReducer(reducer, initialState);
Here, we proved that what you can do with useState
can be done with useReducer
. So, wherever you have useState
, you can just replace it with useReducer
.
Implementing useReducer with useState
Now, let's explore if the opposite is possible—can we replace all instances of useReducer
with useState
? Surprisingly, it's almost true. "Almost" means there are subtle differences. But in general, people expect useReducer
to be more flexible than useState
, so let's see if useState
is flexible enough in reality.
The following example illustrates how to implement the basic capability of useReducer
with useState
:
const useReducer = (reducer, initialState) => {
const [state, setState] = useState(initialState);
const dispatch = (action) =>
setState(prev => reducer(prev, action));
return [state, dispatch];
};
In addition to this basic capability, we can implement lazy initialization too. Let's also use useCallback
to have a stable dispatch function, as follows:
const useReducer = (reducer, initialArg, init) => {
const [state, setState] = useState(
init ? () => init(initialArg) : initialArg,
);
const dispatch = useCallback(
(action) => setState(prev => reducer(prev, action)),
[reducer]
);
return [state, dispatch];
};
This implementation works almost perfectly as a replacement for useReducer
. Your use case of useReducer
is very likely handled by this implementation.
However, we have two subtle differences. As they are subtle, we don't usually consider them in too much detail. Let's learn about them in the following two subsections to get a deeper understanding.
Using the init function
One difference is that we can define reducer
and init
outside hooks or components. This is only possible with useReducer
and not with useState
.
Here is a simple count example:
const init = (count) => ({ count });
const reducer = (prev, delta) => prev + delta;
const ComponentWithUseReducer = ({ initialCount }) => {
const [state, dispatch] = useReducer(
reducer,
initialCount,
init
);
return (
<div>
{state}
<button onClick={() => dispatch(1)}>+1</button>
</div>
);
};
const ComponentWithUseState = ({ initialCount }) => {
const [state, setState] = useState(() =>
init(initialCount));
const dispatch = (delta) =>
setState((prev) => reducer(prev, delta));
return [state, dispatch];
};
As you can see in ComponentWithUseState
, useState
requires two inline functions, whereas ComponentWithUseReducer
has no inline functions. This is a trivial thing, but some interpreters or compilers can optimize better without inline functions.
Using inline reducers
The inline reducer function can depend on outside variables. This is only possible with useReducer
and not with useState
. This is a special capability of useReducer
.
Important Note
This capability is not usually used and not recommended unless it's really necessary.
Hence, the following code is technically possible:
const useScore = (bonus) =>
useReducer((prev, delta) => prev + delta + bonus, 0);
This works correctly even when bonus
and delta
are both updated.
With the useState
emulation, this doesn't work correctly. It would use an old bonus
value in a previous render. This is because useReducer
invokes the reducer function in the render phase.
As noted, this is not typically used, so overall, if we ignore this special behavior, we can say useReducer
and useState
are basically the same and interchangeable. You could just pick either one, based on your preference or your programming style.