How to use the React useReducer hook
The React useReducer
hook is an alternative to the useState
hook. The useReducer
hook is preferable over the useState
hook when dealing with more complex component state logic. Using the useReducer
hook is similar to using reducers in Redux.
useReducer versus useState
Using the useState
hook to manage more complex state can result in a significant amount of code directly in your component body. An example of this is using useState
to manage a list of items that needs to support the addition, modification, and deletion of items.
This complex state management logic should be managed separately from the component, who's main concern should only be to display the list of items. Mixing complex state management logic with presentation logic in the same component can make that component harder to test and to maintain over time.
The useReducer
hook helps us to bring a greater separation of concerns to our component. It does this by allowing us to extract the state management logic outside of our component.
useReducer
const [state, dispatch] = useReducer(reducer, initialState);
The useReducer
hook takes two arguments:
- A reducer: a pure function that updates the state in an immutable way and returns the new state.
- An initial state: the value that the state is initialized with.
The useReducer
hook then returns an array of two elements:
- The current state
- A dispatch function: a special function that dispatches an action object.
What is an action object?
An action object is an object that is used to describe how the state should be updated.
An action object usually has a property type, which is a string that identifies what kind of operation the reducer must perform.
To create an action object to increment a counter in the state, we can do the following:
const action = {
type: 'increment'
};
To create an action object to add a new item in a list, we can use the following action object:
const action = {
type: 'add',
payload: {
name: 'Mario',
},
};
In this case, we needed to pass an additional property into our action object so that we can add a new item with a specific name to our list. This additional property that is passed into an action object is often referred to as a payload. We named the property payload
in this example, but it could have been called anything else, as long as it's a valid JavaScript object property name.
When we want to update the state, we call dispatch(actionObject)
with the right action object. The action object will get forwarded to the reducer function which will update the state.
What is a reducer function?
Here's an example of a reducer function that handles the incrementing and decrementing of a counter.
function counterReducer(state, action) {
const { type, payload } = action;
switch (type) {
case 'increment':
return {
...state,
counter: state.counter + payload
};
case 'decrement':
return {
...state,
counter: state.counter + payload
};
default:
return state;
};
}
The reducer function is a pure function that does not directly modify the state using the state
variable. Instead, it creates and returns a new state object. Avoid directly mutating the state using the state
variable or things will end up not behaving as you might expect them to.
Here's an example of an action object for the dispatch
function that will call the reducer function to increment the counter by 1.
const incrementAction = {
type: 'increment',
payload: 1,
};
Putting the reducer in its own file
We can create a new CounterReducer.js
file and add our reducer function that we created above in this file. Within this file, we can also declare the initial state.
const initialCounterState = {
counter: 0,
};
Let's also add declarations for our action objects within this file.
const incrementAction = {
type: 'increment',
payload: 1,
};
const decrementAction = {
type: 'decrement',
payload: 1,
};
Using the useReducer hook
We can now make use of the useReducer
hook in a React component, which will make use of our reducer function.
import { useReducer } from "react";
import {
counterReducer,
initialCounterState,
incrementAction,
decrementAction,
} from '../reducers/CounterReducer.ts';
export const Counter = () => {
const [state, dispatch] = useReducer(counterReducer, initialCounterState);
return (
<>
<p>Counter: {state.counter}</p>
<button onClick={() => dispatch(incrementAction)}>+</button>
<button onClick={() => dispatch(decrementAction)}>-</button>
</>
);
}
When we use the useReducer
hook within our Counter
component, we provide it with two parameters:
- Our reducer function that we called
counterReducer
. - The initial state that we called
initialCounterState
.
The dispatch
function that we receive from the useReducer
hook will allow us to forward our action objects to the reducer function. When the state is updated by the reducer function, the component will re-render, and the state
variable that we get from useReducer
will reflect the newly updated state.
Using TypeScript
We can also use TypeScript to add types for our state and action objects. To do so, we can save our CounterReducer.js
as CounterReducer.ts
and use the following code.
type State = {
counter: number;
}
const initialCounterState: State = {
counter: 0,
}
enum ActionType {
Increment = 'INCREMENT',
Decrement = 'DECREMENT',
}
type Action = {
type: ActionType,
payload: number,
}
const incrementAction: Action = {
type: ActionType.Increment,
payload: 1,
}
const decrementAction: Action = {
type: ActionType.Decrement,
payload: 1,
}
function counterReducer(state: State, action: Action): State {
const { type, payload } = action;
switch (type) {
case ActionType.Increment:
return {
...state,
counter: state.counter + payload
};
case ActionType.Decrement:
return {
...state,
counter: state.counter - payload
};
default:
return state;
};
}
Conclusion
useReducer
makes it easier to manage more complex state. The simple example above, where useReducer
is used to manage a counter's state, was a trivial example only meant to demonstrate how to make use of the useReducer
hook.
For simple state management within your components, you can stick to using the useState
hook.