This site runs best with JavaScript enabled.

Should I useState or useReducer?

February 11, 2019

Video Blogger

Photo by Kyle Glenn on Unsplash


Two built-in React hooks that handle state, which one should you use?

Whenever there are two things to do the same thing, people inevitably ask: "When do I use one over the other?"

For example "When do I use useEffect and useLayoutEffect?" Or "When do I use Unit, Integration, or E2E tests?" Or "When to use Control Props or State Reducers?"

I think useState and useReducer are no exception to this at all. In fact, Matt Hamlin already posted useReducer, don't useState and he makes some great points there. I'd like to throw my hat in this discussion though because I was asked about it on my AMA.

Congratulations on the new design of your website kentcdodds.com. Looking at the source code of your website, in the Subscribe component, you used useState hooks to handle the state of this component. My question is, is not it more optimized to use a useReducer here instead of several useState? If not, why?

Here's the top of that Subscribe component

1const [submitted, setSubmitted] = React.useState(false)
2const [loading, setLoading] = React.useState(false)
3const [response, setResponse] = React.useState(null)
4const [errorMessage, setErrorMessage] = React.useState(null)

Then there's logic throughout the component for calling those state updater functions with the appropriate data, like here:

1async function handleSubmit(values) {
2 setSubmitted(false)
3 setLoading(true)
4 try {
5 const responseJson = await fetch(/* stuff */).then(r => r.json())
6 setSubmitted(true)
7 setResponse(responseJson)
8 setErrorMessage(null)
9 } catch (error) {
10 setSubmitted(false)
11 setErrorMessage('Something went wrong!')
12 }
13 setLoading(false)
14}

If I were to rewrite this to use useReducer then it would look like this:

1const [state, dispatch] = React.useReducer(reducer, {
2 submitted: false,
3 loading: false,
4 response: null,
5 errorMessage: null,
6})

Then the reducer would look something like this:

1const types = {
2 SUBMIT_STARTED: 0,
3}
4function reducer(state, action) {
5 switch (action.type) {
6 case types.SUBMIT_STARTED: {
7 return {...state, submitted: false, loading: true}
8 }
9 case types.SUBMIT_COMPLETE: {
10 return {
11 ...state,
12 submitted: true,
13 response: action.response,
14 errorMessage: null,
15 loading: false,
16 }
17 }
18 case types.SUBMIT_ERROR: {
19 return {
20 ...state,
21 submitted: false,
22 errorMessage: action.errorMessage,
23 loading: false,
24 }
25 }
26 default: {
27 return state
28 }
29 }
30}

And the handleSubmit function would look like this:

1async function handleSubmit(values) {
2 dispatch({type: types.SUBMIT_STARTED})
3 try {
4 const responseJson = await fetch(/* stuff */).then(r => r.json())
5 dispatch({type: types.SUBMIT_COMPLETE, response: responseJson})
6 } catch (error) {
7 dispatch({type: types.SUBMIT_ERROR, errorMessage: 'Something went wrong!'})
8 }
9}

Matt Hamlin brings up in his blog post a few benefits to useReducer over useState:

  • Easier to manage larger state shapes
  • Easier to reason about by other developers
  • Easier to test

For this specific case I don't think that the first or second point really applies. Four elements of state is hardly a "large state shape" and the before/after here is no easier or harder to "reason about" for me. I think they're equally simple/complex.

As for testing, I would definitely agree that you could test the reducer in isolation and that could be a nice benefit if I were doing a bunch of business logic in there, but I'm not really. It's pretty simple there.

Typically I prefer to write higher-level integration-like tests, so I wouldn't want to write tests for that reducer in isolation and instead would test the <Subscribe /> component and my tests would treat the reducer as an implementation detail.

Now if there were some complex business logic in that reducer or several edge cases, then I definitely would want to test that in isolation (and I would use jest-in-case to do it!).

I think there is one main situation in which I prefer useState over useReducer:

When prototyping/building the component and you're not certain of the implementation

While building a new component, you're often adding/removing state from that component's implementation. I think it would be harder to do that if you do this with a reducer. Once you solidified what you want your component to look like then you can go make the decision of whether converting from several useStates to a useReducer makes sense. Additionally, maybe you'll decide that useReducer makes sense for some of it and a custom hook that uses useState would make sense for other parts of your component logic. I find it's almost always better to wait until I know what my code is going to look like before I start making abstractions.

Oh, and if you're prototyping, the code can be as unmaintainable as you want :) So who cares? Do what's faster.

One situation when useReducer is basically always better

If your one element of your state relies on the value of another element of your state, then it's almost always best to use useReducer

For example, imagine you have a tic-tac-toe game you're writing. You have one element of state called squares which is just an array of all the squares and their value:

1[
2 ' ', 'X', 'O',
3 'X', 'O', 'X',
4 ' ', ' ', 'X'
5]

and another called xIsNext which maintains the who's turn it is. When a user clicks on a square, how does your code know whether the squares array should update to X or O? It determines this based on the xIsNext state. Because of this, it's easier to use a reducer because the reducer function can accept all of the current state and use that current state (which includes xIsNext) to determine the new state.

The benefits here are mostly just code aesthetic, but if you start adding async behavior here, then the case for useReducer is even more strong. With our tic-tac-toe game, you can reference the current value of xIsNext in the closure, but if you are updating the squares state asynchronously, then you could be working with stale values of state which may or may not be what you want. Using a reducer completely removes this potential issue though, which is why I say it's basically always better to use a reducer if your state elements depend on one another when they're updated.

Here's an example of tic-tac-toe with useReducer:

Conclusion

So what's the answer? Really, it depends. useState is literally built on top of useReducer. I don't think there are any relevant performance concerns between the two so it's mostly a cosmetic/preferential decision.

While I conceptually like what Matt is encouraging, I think I may have a longer threshold before I'll reach for useReducer to replace my useState. I also really appreciate Matt for including this:

they both have benefits and fallbacks that depend entirely upon their use

I think the best thing you can do to develop an intuition for when to reach for one or the other is to feel the pain. Use them both and see how happy/sad they make your life.

Good luck!

Discuss on TwitterEdit post on GitHub

Share article
loading relevant upcoming workshops...
Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. He's taught hundreds of thousands of people how to make the world a better place with quality software development tools and practices. He lives with his wife and four kids in Utah.