Sebastian Pieczynski's website and blog
Published on: yyyy.mm.dd
Published on: yyyy.mm.dd
Created with ❤️ by Sebastian Pieczyński © 2023-2024.
Published on: 1/25/2024
You probably came here to learn how to use reducer (pun intended) hook in React. Before we get into that let's talk about similar function in Javascript: Array.reduce .
.reduce(reducerFunction, initialValue)
is a method on an Array object that allows to "reduce" values from an array into a single value. One of the uses could be calculating sum of an array. It accepts two arguments: a function that will be executed for every element of an array (reducerFunction
in our case) and an initial value (initialValue
) that the reducer function will use for first calculation. After the reduce function is done traversing all the elements in the array it returns a single value.
Note that reduce
should be used sparingly and it's a tool not a doctrine. Even with the example above it may not be obvious that this is just a sum function.
Now that we have some understanding how reducer functions work and how they work let's dive into the useReducer
hook.
useReducer
is a hook that allows us to manage complex states. It is used for more complex states like ex. cart contents, user data or any other state that requires changing additional state data inside an object . For such cases it is the best option.
Contrary to the reduce function above useReducer
accepts three arguments and it returns 2 values:
Let's break it down:
state
:
this is a current state returned by the reducer based on last actions performed (or initial state if no actions were sent, more on that in a sec).
dispatch
:
a function that allows us to send type
and an action
or payload
that will determine how our state
should change. Example could be dispatch({type: 'increment', payload:{step: 2}})
. Type is usually a verb describing type of action we want to perform.
dispatch
argument: payload
/action
:
argument of the dispatch
function - variable or more commonly an object typically consisting of data describing the way that the state should be changed. Type is usually a verb describing type of action we want to perform ex. increment or decrement the counter and payload is a name of the variable(s) that we pass to the action. It defines for example the step or any other variable(s) that would need to affect the state to transition from current to new values when incrementing or decrementing the counter.
reducerFunction
:
function that will be responsible for changing state from current one to new one based on the type
and optional payload
.
initialState
:
object with a state that should be set when first invoking the reducer.
initializerFunction
:
OPTIONAL function that will initialize state, should be used when state requires computation or manipulation inside a function.
To show how useReducer changes the way we interact with the state it will be best to show an example of before and after.
Using good old counter will also help in showing the principle without focusing on implementation details / complexity.
We'll add features to the counter - ability to set the step for counting and resetting the counter to initial values. Initial count and initial step will be set as component props. We'll implement same functionality with useReducer
after this.
Nothing really interesting here, but note that we had to implement all the functions in the component to keep it cleaner and while a bit contrived when implementing reset function we had to remember to invoke both setCount
and setStep
functions. Forgetting one of the calls to setState
here would make our reset function incomplete and not work as expected. When adding new features or refactoring the component it would not be uncommon to miss a call to setState
at some other function without even realizing it.
Using reducers comes with a seemingly much larger boilerplate. But... in complex projects that boilerplate actually reduces the complexity and amount of mental overhead we need to understand what happens in the components. We'll also separate reducers and TypeScript types to separate modules (files).
First let's create types that will describe the shape of the data as well as allowed actions.
So the CounterStateType
describes the shape of the data we expect to have in state when working with our reducer and CounterActionType
defines what types of actions as well as what payload (if any) we are expecting.
Now let's implement the reducer function that will modify our state according to action we have dispatched. Remember that reducers MUST be pure . That means that given same arguments they will return the same result. In our case we are passing the initialState
as the payload for reset
action. We could export that variable and import it in the reducer but that would couple it to the reducer code and it would become impure.
This is again a bit contrived but remember about it when implementing your own reducers - if you need to do some complex data modifications that modify state values in unpredictable ways (ex. returning current date - pass it as payload and do not calculate it inside the reducer itself).
Now that all parts of the state management are prepared we can code our reducer with useReducer
hook. What I do love about reducers is that they create a location where all the logic for managing the state changes lives. It makes it easy then to add new cases and reason about the component's responsibility.
As you can see even in case of a very simple counter component we were able to simplify it:
dispatch
-ed. This reduces errors, minimizes involvement required by the person using the component, limits the ability to pass incorrect data to the component.Hope you had fun and learned something new today!
You are doing great and you are a great person so keep it up!
Back to Articles