React State useState vs useReducer

Lizzie Hard
4 min readJun 30, 2021

So you have a React application and you need to store some state for components so that users can interact with the page. This may be something like the scenario of clicking a button to filter your results or tracking the value of a text area so that it updates with the users input.

State can be a confusing thing for many reasons. You may have lots of different elements which have their own states that you need to keep track of. Alternatively you may have lots of different components that need to access the same state. These different components may either be further down the tree or in a completely unrelated part of the application. Whatever the reason, tracking state can get complicated quickly.

Lots of lines connecting smiley face nodes

Since React introduced hooks back in October 2018 there has been 2 choices for managing your component state, these are useState and useReducer.

Advantages of useState

  • Good for managing primitive state values in a component such string, integers or booleans
  • Simple and easy therefore it enables fast development as you can get up and running with it quickly
  • If two components need to share state you can “lift state up” to the closest common parent and pass down state through props. I’ve written another blog post exploring this here.

Advantages of useReducer

  • Good for managing more complicated states, such as objects with nested values
  • Useful for managing different actions on a state, often lots of useState declarations in one component could be combined into one useReducer function with different actions
  • Can pass a dispatch function through your components rather than using setState callbacks throughout your app which could lead to performance improvements

useState in-depth

Setting up the useState hook is nice and simple:
const [value, setValue] = useState('');
Here you have the initial state in value which we’ve set to an empty string in useState and the setter function is setValue.

To update the state and change value we can use the setter function setValue whenever the input value is changed.

<form onSubmit={onSubmit}>
<textarea
placeholder='Enter new note …'
name='listItem'
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</form>

For every subsequent state you have to set up a new value and setter function:

const [tasks, setTasks] = useState([]);const onSubmit = (e) => {
e.preventDefault();
if (value) {
setTasks([…tasks, value])
}
setValue('');
};

On pressing the button to submit the form here we check that it’s not an empty value and then add the new task (value) to the tasks array. The whole code snippet can be found in the codesandbox here:

We only have two state declarations in this component but you could see how this could quickly get very complicated very quickly with multiple states. If you find this is the case it may be a good reason to combine them all in one useReducer hook.

useReducer in-depth

Let’s take a look at how to set up the useReducer hook in your functional component. It defines a state (here we have defined it as tasks) and a dispatch method and it takes inputs of a reducer function and the initial state variable.
const [tasks, dispatch] = useReducer(reducer, [])

So here our initial state is tasks = []

In order to trigger a state update you have to dispatch an action. An action always has a type and has an optional value of a payload. The payload allows you to pass through values to use in the reducer to mutate the state.

dispatch({type: ‘add’, payload: { task: value}})

Finally the reducer function could look something like this:

function reducer(tasks, action) {
switch (action.type) {
case 'add':
return [action.payload.task, ...tasks]
default:
return tasks;
}
}

It only has one action type but it could easily have multiple types enabling you to use the same useReducer function to mutate the state in different ways. Here when an action of type ‘add’ is dispatched to the reducer function it mutates the state but adding a new task to the task array.

There is a full working example of a simple notes app with useReducer which you can make edits to in codesandbox here:

Simple notes application using useReducer

Conclusion

Depending what your needs are for state should help you determine which is the best choice for you. These are simple solutions for state management to manipulate your react component, you may need something with a bit more scope in order to be able to read your state all over your application easily.

Both of these solutions could be combined with another hook, useContext, and used as a lightweight alternative to a third party application such as Redux. This is useful for smaller apps with less complicated global states where a large third party application could bloat them unnecessarily.

Using this method you could manage your application state in one central place and pass it around the rest app with the useContext hook. This could help you prevent issues with prop drilling as if you make a change you do not have to check everywhere you have passed it as a prop.

However these may not be the right solution to every state management problem. If you have a large application and one a single source of truth for your state using a third party library is a very good option. You may also need to consider persistence, these solutions would not persist between sessions or even page refreshes, other libraries have inbuilt solutions to all these issues and more.

--

--