useReducer Explained

Sam Dent
The Startup
Published in
5 min readNov 8, 2020

This is an article for those new to React and hooks.

I’ll attempt to break down the goings on of the useReducer hook so that you can feel more comfortable using it. To see it in action in a common example of managing input state for a form visit this other post.

So you’ve gotten to grips with the useState hook and it’s truly wonderful, it is really… but as your applications grow in size you’ve noticed line after line of useState. Not only is this not very DRY but sometimes each piece of state is just solving a smaller part of the same bigger problem.

If this is you, let me introduce you to useReducer a hook that solves your complex state needs.

useReducer can be initially a little confusing but if you’re comfortable with the pattern of

const [state, setState] = useState(initialState)

then you will soon wrap your head around the useReducer pattern of

const [state, dispatch] = useReducer(reducer, initialState)

There’s just a wee bit more to configure to get started with useReducer, but the small investment in time pays off in terms of that sweet sweet maintainable, clean code.

But first let’s talk broad strokes.

WHATS GOING ON!?

All useReducer does for us is take an initial state and give us a way to alter that state based on the rules we set. That’s just useState done fancy.

Yo, useReducer, here’s some state, here are my rules, what does the new state look like?

REDUCERS, ACTIONS, AND DISPATCHES!? OH MY

Let’s delve into what makes up your useReducer hook starting with the reducer function. The first argument passed to the hook

const [state, dispatch] = useReducer(reducer, initialState)

This is a function defined by you elsewhere in your code (maybe even in it’s own neat file, yay for modularity!) call it whatever you’d like to give it meaning to you, it’s still a reducer function conventionally.

It follows the pattern of

const reducer = (state, action) => {
...
// update the state with rules dictated by action
...
return updatedState;
}

You pass it your state (normally an object), you tell it what to do (the action bit) and you return from it the updated shiny new state.

Typically action will be an object that has a label or type and maybe a payload, some data to update your state with in some way.

{type: "INCREASE STATE BY PAYLOAD", payload: 5}

We’ll see this again in a minute, the important thing to understand is an action helps you define how to change your state, with a handy descriptive type, and a payload of new/additional data if necessary.

Let’s now take a look at how this might update our state in our reducer function.

The essence here is that depending on the type of action (action.type) you want to execute a different code block then return a new state. It’s common that you’ll see switch statements involved here since they fit that purpose swimmingly.

Imagine you have an application that tracks personal finance, you could have an initial piece of state with various values like so:

const initialState = {
moneyInBank: 0,
moneyInSofa: 999,
billsToPay: 1000
}

so you might want your reducer to handle all of these values predictably.

const reducer = (state, action) => {
switch(action.type){
case "DEPOSIT MONEY IN BANK":
return {...state,
moneyInBank: state.moneyInBank + action.payload
};
case "PAY SOME BILLS":
return {...state,
billsToPay: state.billsToPay - action.payload
};
case "CLEAN THE SOFA":
return {...state,
moneyInBank: state.moneyInBank
+ state.moneyInSofa,
moneyInSofa: 0
};
};
}

Inside our reducer function we let the switch statement do the heavy lifting, it uses the type property of our action object to decide what needs to happen to the state. It matches that type value with the appropriate case and executes the necessary return statement.

For each case you’ll see that we are returning a new object, this prevents us from mutating state and angering the React gods in the event. Don’t mutate the state! Instead we use the nifty spread operator (…state) which copies all the existing properties and values into our new object. Then we overwrite the property we want to update by explicitly declaring it again with the new value (moneyInBank: state.moneyInBank + action.payload)

The fact that we write this function external to our useReducer hook again encourages a separation of concerns. We can keep all the logic for updating our state in one neat modular place especially if we were to have this reducer in its own file.

So all this is good, we get it. A reducer is just a function that is passed some state and an action object which helps define how to update the state. We give that function to the useReducer hook as the first argument. Sweet as.

But… how do we execute that function, where are actions actually coming from. What in blazes is a dispatch!?

Easy now, it’s exciting stuff I know and will become clearer in good time.

Cast your mind back to 2 minutes ago when useState was our favourite hook and the world was a simpler place. When we call useState we destructure from it the state value and conventionally named setState function to update the state value in a React friendly way.

const [state, setState] = useState(initialState);

Take a look again at the useReducer hook.

const [state, dispatch] = useReducer(reducer, initialState);

Instead of a setState we have a dispatch but you can logically think of them achieving the same goal.

We call dispatch() in our code when we want to change state. We already have a reducer set up to do the legwork of changing the state, we just need to be able to trigger how to change it.

Baby. We’re ready for action.

Whenever we want to change our complex state in the code we have the handy dispatch method to send actions off to the reducer.

Following the reducer example from earlier. There’s a point in my personal finance app where a user should have the ability to pay bills by a given amount. In my code I can call dispatch with an action like so

...
dispatch({type: "", payload: 100});
...

And that’s it, state taken care of. And it can get as big and complex as you like from now, all you’ll ever need to do is write an action for you reducer to identify. Add the case to your reducer function and dispatch the action when needed.

3 ingredients and you can make your code so much cleaner, more maintainable and easier to read.

It’s all down to those 3 things reducer, action, dispatch.

Good things come in 3s. 3 is the magic number. 2s company but 3’s a party…

It can be useful to see examples and I find that forms are a pretty good use case for a reducer in React so if you’d like to see my example of translating multiple useStates into one sexy useReducer then take a look here.

Otherwise I hope you find something helpful in this article, and feel encouraged to get friendly with useReducer soon!

--

--

Sam Dent
The Startup

Adventuring in the world of software development.