sebee.website🌱
  • Articles
  • CV
  • About
  • Contact

Sebastian Pieczynski's website and blog

Published on: yyyy.mm.dd

Back to Articles
sebee.website
  • Created with ❤️ by Sebastian Pieczyński © 2023-2025.

Links

  • Terms of Use
  • Privacy Policy
  • Cookie Policy
  • ethernal
  • @spieczynski

React 'useReducer' hook explained

Published on: 1/25/2024

two sets of universes one on the left side enclosed inside a 3d hexagonal translucent box and a second one loose and scrambled all over the place without order, watercolor

Contents

  1. Groundwork
  2. useReducer hook
    1. useReducer definition
    2. Implementing 'Counter' with useState
    3. Implementing 'Counter' with useReducer (and Typescript)

Groundwork

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.

useReducer hook

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.

useReducer definition

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.

Implementing 'Counter' with useState

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.

Implementing 'Counter' with useReducer (and Typescript)

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:

  1. There are no functions being invoked inside the component only the "intent" is dispatch-ed. This reduces errors, minimizes involvement required by the person using the component, limits the ability to pass incorrect data to the component.
  2. Logic for managing the component is collocated and in a single file.
  3. We were able to improve types for the component and it's use is safer and easier.
  4. We have simplified the way we interact with the component - no need to write our own functions to increment or decrement, everything is encapsulated in the reducer.
  5. We learned that when using reducers we need to respond with predictable states ex. do not generate dates or other random data inside reducers but pass it as payload from 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
StateCounter.tsx
1import { useState } from 'react';
2
3function StateCounter({ initialCount = 0, initialStep = 1 }) {
4 const [count, setCount] = useState(initialCount);
5 const [step, setStep] = useState(initialStep);
6
7 const incrementCount = () => setCount((previous) => previous + step);
8 const decrementCount = () => setCount((previous) => previous - step);
9 const incrementStep = () => setStep((previous) => previous + 1);
10 const decrementStep = () => setStep((previous) => previous - 1);
11
12 const reset = () => {
13 setCount(initialCount);
14 setStep(initialStep);
15 };
16
17 return (
18 <>
19 <div>
20 <h2 className="pb-2">StateCounter</h2>
21 </div>
22
23 <div className="flex flex-row gap-2 justify-center items-center">
24 <button
25 onClick={() => {
26 decrementCount();
27 }}
28 type="button"
29 className="pb-2 px-4 pt-1 text-3xl bg-slate-700"
30 >
31 -
32 </button>
33 <button
34 onClick={() => {
35 incrementCount();
36 }}
37 type="button"
38 className="pb-2 px-4 pt-1 text-3xl bg-slate-700"
39 >
40 +
41 </button>
42 </div>
43
44 <p className="pt-4">Step: {step}</p>
45 <div className="flex flex-row gap-2 justify-center items-center">
46 <button
47 onClick={() => {
48 decrementStep();
49 }}
50 type="button"
51 className="pb-2 px-4 pt-1 text-3xl bg-slate-700"
52 >
53 -
54 </button>
55 <button
56 onClick={() => {
57 incrementStep();
58 }}
59 type="button"
60 className="pb-2 px-4 pt-1 text-3xl bg-slate-700"
61 >
62 +
63 </button>
64 </div>
65 <h3 className="pt-4">
66 <p>Count: {count}</p>
67 </h3>
68 <div className="flex flex-row gap-2 justify-center pt-4">
69 <button
70 onClick={() => {
71 reset();
72 }}
73 type="button"
74 className="pb-2 px-4 pt-1 text-3xl bg-slate-700"
75 >
76 reset
77 </button>
78 </div>
79 </>
80 );
81}
82
83export default StateCounter;
ReducerCounter.tsx
1import { useReducer } from 'react';
2
3import { counterReducer } from '../reducers/counterReducer';
4
5import type { CounterStateType } from '../types/CounterTypes';
6
7function ReducerCounter({
8 initialCount = 0,
9 initialStep = 1,
10}: {
11 initialCount?: number;
12 initialStep?: number;
13}): JSX.Element {
14 const initialState: CounterStateType = {
15 count: initialCount,
16 step: initialStep,
17 };
18
19 const [state, dispatch] = useReducer(counterReducer, initialState);
20
21 return (
22 <>
23 <div>
24 <h2 className="pb-2 pt-8">Reducer Counter</h2>
25 </div>
26
27 <div className="flex flex-row gap-2 justify-center items-center">
28 <button
29 onClick={() => {
30 dispatch({ type: 'decrementCount' });
31 }}
32 type="button"
33 className="pb-2 px-4 pt-1 text-3xl bg-slate-700"
34 >
35 -
36 </button>
37 <button
38 onClick={() => {
39 dispatch({ type: 'incrementCount' });
40 }}
41 type="button"
42 className="pb-2 px-4 pt-1 text-3xl bg-slate-700"
43 >
44 +
45 </button>
46 </div>
47
48 <p className="pt-4">Step: {state.step}</p>
49 <div className="flex flex-row gap-2 justify-center items-center">
50 <button
51 onClick={() => {
52 dispatch({ type: 'decrementStep' });
53 }}
54 type="button"
55 className="pb-2 px-4 pt-1 text-3xl bg-slate-700"
56 >
57 -
58 </button>
59 <button
60 onClick={() => {
61 dispatch({ type: 'incrementStep' });
62 }}
63 type="button"
64 className="pb-2 px-4 pt-1 text-3xl bg-slate-700"
65 >
66 +
67 </button>
68 </div>
69 <h3 className="pt-4">
70 <p>Count: {state.count}</p>
71 </h3>
72 <div className="flex flex-row gap-2 justify-center pt-4">
73 <button
74 onClick={() => {
75 dispatch({ type: 'reset', initialState: initialState });
76 }}
77 type="button"
78 className="pb-2 px-4 pt-1 text-3xl bg-slate-700"
79 >
80 reset
81 </button>
82 </div>
83 </>
84 );
85}
86
87export default ReducerCounter;
MDN_Example.ts
1const array1 = [1, 2, 3, 4];
2
3// 0 + 1 + 2 + 3 + 4
4const initialValue = 0;
5const sumWithInitial = array1.reduce(
6 (accumulator, currentValue) => accumulator + currentValue,
7 initialValue,
8);
9
10console.log(sumWithInitial); // Expected output: 10
1const [state, dispatch] = useReducer(reducerFunction, initialState, initializerFunction)
src/types/counterTypes.ts
1type CounterStateType = {
2 count: number;
3 step: number;
4};
5
6type CounterActionType =
7 | { type: 'decrementCount' }
8 | { type: 'incrementCount' }
9 | { type: 'decrementStep' }
10 | { type: 'incrementStep' }
11 | { type: 'reset'; initialState?: CounterStateType };
12
13export type { CounterStateType, CounterActionType };
src/reducers/counterReducer.ts
1import type {
2 CounterActionType,
3 CounterStateType,
4} from '../types/CounterTypes';
5
6function counterReducer(
7 state: CounterStateType,
8 action: CounterActionType,
9): CounterStateType {
10 switch (action.type) {
11 case 'decrementCount':
12 return { ...state, count: state.count - state.step };
13 case 'incrementCount':
14 return { ...state, count: state.count + state.step };
15 case 'decrementStep':
16 return { ...state, step: Math.max(state.step - 1, 1) };
17 case 'incrementStep':
18 return { ...state, step: state.step + 1 };
19 case 'reset':
20 return action.initialState ? action.initialState : { count: 0, step: 1 };
21 default:
22 return state;
23 }
24}
25
26export { counterReducer };