Back to overview

How to implement useState with useReducer

August 30th, 2019 6 min read

by Marc Sendra Martorell
by Marc Sendra Martorell
No translations available.Add translation
💿 This blog post involves React, but was written before Remix was launched. Learn how Remix drastically simplifies React applications from the post:
Remix: The Yang to React's Yin ☯

Watch "Implement useState with useReducer" on egghead.io

Here's the TL;DR:

const useStateReducer = (prevState, newState) =>
  typeof newState === 'function' ? newState(prevState) : newState

const useStateInitializer = initialValue =>
  typeof initialValue === 'function' ? initialValue() : initialValue

function useState(initialValue) {
  return React.useReducer(useStateReducer, initialValue, useStateInitializer)
}

Wanna dive in? Let's go.

But Kent... Why?

For fun 🤓 Also I think that re-implementing things is a great way to learn how they work.

State management in React

React hooks expose two mechanisms for state management: useState and useReducer. Interestingly enough, React actually builds useState out of the same code that's used to build useReducer. They do this because managing a single value of state in a component is very common, but doing that with useReducer would require a bit of boilerplate. So they reduce the boilerplate by exposing a simpler state management API through useState.

They have the benefit of having all their internal code to do this, but we can do this ourselves as well 😄

The useState API

Let's start off by looking at the API that useState exposes to us:

useState function arguments:

You can call useState three different ways:

useState() // no initial value
useState(initialValue) // a literal initial value
useState(() => initialValue) // a lazy initial value

Read more about lazy initial state

So our new useReducer-based useState will need to support all of these argument variations.

useState return value

When you call useState it returns the state and a mechanism for updating that state (commonly called a "state updater function"). That function can be called with the new state or a function which accepts the previous state and returns the new state. So our new useReducer-based useState will need to support both of these variations.

const [state, setState] = useState()
setState(newState)
setState(previousState => newState)

This is similar to what useReducer does as well, except the mechanism for updating the state is called a "dispatch" function and instead of being used to set the state directly, it delegates the actual state update logic to the reducer.

The useReducer API

So here's the useReducer API:

const [state, dispatch] = React.useReducer(reducerFn, initialValue)

And with useReducer, if you want to have lazy initialization, then you provide a third argument which is your initialization function and the second argument serves as an argument to that initialization function, so you can rename that to something like initialArg.

const initializationFn = initialArg => initialArg

const [state, dispatch] = useReducer(reducerFn, initialArg, initializationFn)

And remember, the reducerFn is responsible for what the dispatch function does. So if you want to control how the state is updated by the dispatch function, you can do that via the reducerFn which is called with whatever dispatch is called with.

const reducerFn = (prevState, dispatchArg) => newState

With that, we can implement all the features of useState.

The useReducer-based useState implementation

Here's our starting point:

const useStateReducer = () => {}

function useState() {
  return React.useReducer(useStateReducer)
}

Let's start by trying to implement this use case for the state update function:

const [count, setCount] = useState(0)
setCount(count + 1)

So we need to make the dispatch function actually update the state value. To do that, we make our reducer take the dispatchArg and return that.

const useStateReducer = (prevState, dispatchArg) => dispatchArg

function useState() {
  return React.useReducer(useStateReducer)
}

With that it actually makes more sense to call dispatchArg newState instead:

const useStateReducer = (prevState, newState) => newState

function useState() {
  return React.useReducer(useStateReducer)
}

Great! Next, let's support the function update version of the useState API:

const [count, setCount] = useState(0)
setCount(previousCount => previousCount + 1)

If we want to continue to support the previous API, we'll need to do some typeof checking to determine whether it's a function and if it is we'll call it with the previous state. Otherwise we'll just return it. Ternaries to the rescue!

const useStateReducer = (prevState, newState) =>
  typeof newState === 'function' ? newState(prevState) : newState

function useState() {
  return React.useReducer(useStateReducer)
}

Nice! Now let's move on to that initial value! For the simple useState(0) case, it's actually really straightforward:

const useStateReducer = (prevState, newState) =>
  typeof newState === 'function' ? newState(prevState) : newState

function useState(initialValue) {
  return React.useReducer(useStateReducer, initialValue)
}

That's it. But what about the lazy version? useState(() => 0) That one's a little more tricky because the useReducer API is slightly different here. Let's iterate to that first. Here's another way we could implement the non-lazy useState(0) use case:

const useStateReducer = (prevState, newState) =>
  typeof newState === 'function' ? newState(prevState) : newState

const useStateInitializer = initialArg => initialArg

function useState(initialValue) {
  return React.useReducer(useStateReducer, initialValue, useStateInitializer)
}

In this case we're passing the initialValue as the initialArg and our useStateInitializer function is simply returning that value. This makes it easier to support the lazy initializer version of the API. We simply need to determine whether the initialArg is a function and if it is, we'll call it, otherwise we'll return it.

const useStateReducer = (prevState, newState) =>
  typeof newState === 'function' ? newState(prevState) : newState

const useStateInitializer = initialValue =>
  typeof initialValue === 'function' ? initialValue() : initialValue

function useState(initialValue) {
  return React.useReducer(useStateReducer, initialValue, useStateInitializer)
}

And that's it!

Conclusion

I hope you enjoyed digging around these APIs a little bit more with me. I definitely recommend you just continue using the built-in useState hook, but I thought you'd find it interesting to see how flexible useReducer is. You don't have to use it the same way you used redux (in fact, you don't have to use redux in the conventional way either... or at all).

And just for fun, you can play around with this on CodeSandbox if you wanna:

Edit useState implemented by useReducer

Good luck!

💿 Don't forget to checkout Remix: The Yang to React's Yin ☯
Kent C. Dodds
Written by Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. He's Co-Founder and Director of Developer Experience at Remix! Kent'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.

Learn more about Kent

If you found this article helpful.

You will love these ones as well.