There's a misconception these days in the React community that whenever you want to manage a complex object instead of breaking down to individual state variables, you should use useReducer
.
But, as I'll show in this article, managing a complex object and the changes it can go through can be more easily done using useState
.
Show me the code
OK, here are the 2 versions, which are pretty much equivalent in result:
useReducer:
function reducer(state, action) {
switch(action.type) {
case 'MOVE_RIGHT':
return { ...state, left: state.left + action.step };
case 'MOVE_DOWN':
return { ...state, top: state.top + action.step };
default:
return state;
}
}
const [position, dispatch] = useReducer(reducer, { left: 0, top: 0 });
dispatch({ type: 'MOVE_RIGHT', step: 10 });
useState:
const [position, setPosition] = useState({ left: 0, top: 0 });
const actions = useMemo(() => ({
moveRight: step => {
setPosition(state => ({ ...state, left: state.left + step }))
},
moveDown: step => {
setPosition(state => ({ ...state, top: state.top + step }))
}
}), []);
actions.moveRight(10);
Note the
useMemo
. The reason it is there is to make sure we gain feature parity withuseReducer
. Since thedispatch
parameter ofuseReducer
is always constant throughout renders, I wanted the actions to be that as well
So what did we gain?
Easier TypeScript support.
Inside the reducer, you are dealing with different types of actions, each with its own parameters. To get that working well with TypeScript you'll need to add a type per action and follow some rules to make sure TypeScript can differentiate between the different action types inside the switch case.
It's less intuitive and more verbose than working with plain function signatures, which have the parameter types colocated next to the implementation.Better IDE Support
If you use functions and not action objects, you can jump to its definition, look for references to a function, and rename it globally with the help of the IDE.Less error-prone
String action types are prone to undetected errors. You can obviously bypass this with some constants or TypeScript magic, but that means you need to add more boilerplate code.
Did we lose anything?
It's easier to test a reducer. Or is it?
Since a reducer is a pure function, it's easier to test. Testing a function that uses the setter function will require some extra wiring.
But, we could easily write a test-util once that will help us test object actions, and reuse it where-ever we need. Moreover, the benefits of functions over action objects will also prove useful inside tests, such as better TS and IDE support.
So all in all, I would argue that the benefits surpass the drawbacks in terms of testing.
What about reducers in Redux?
For a global state manager, there are other benefits to using action objects. Several reducers can handle the same action, you get a nice view of the history of the app state using the devtools, and it's easy to export and import entire user flows. You can think of every state mutation as a user intent that is expressed using an object - and it opens up more possibilities.
But for a local state, these benefits do not exist. You always handle the action in the same reducer, and the action history of a single component is not that interesting.
Change my mind
I know it's a very common pattern in the industry to use a useReducer
, but I really hate doing things just because they're popular if they don't make any sense to me.
So I'm very very very much open to other opinions and ideas, and if you can change my mind I'd love to see how.
Top comments (6)
Well this pattern works well for a simple object like yours. If it's a deeply nested object and for any reason you need to keep the integrity of the object, reducer composition is something that would add lots of value to you.
Can you give an example?
If I understand you correctly, then you can also compose the setter function of useState and add functionality on top like data integrity.
Nice, I like it.
I have yet to come across a situation where I need useReducer honestly. If it's a local state I'll use useState with a few state variables. If it's too complex then it probably needs a refactor to more components or belongs in redux
Can you explain why you also used the memo hook? It seems to me the point of clarity you made and the unnecessary use of useReducer was somewhat obfuscated by that sneaky little inclusion ;)
Hi,
not trying to be sneaky, just concise :)
But if that's confusing I'll add a note to the article
I wanted to show 2 equivalent solutions, and because dispatch never changes I wanted to ensure that actions never change with useMemo
Nice article.
I have seen a situation where one have to call several useState to handle seperate states in a component, in such situation, won't useReducer be the best hook to use?