- Introduction
- The React useReducer Syntax
- What is a state in React?
- What is a dispatch in React?
- The reducer function
- The initial state
- Lazy initialization
- How the React useReducer hook works
- Bailing out of a dispatch
- useReducer with Typescript
- useState Vs useReducer
- When to use the useReducer hook
- When not to use React useReducer
- Recommend videos
- Interesting reads from our blogs
- Conclusion
Introduction
State management has been a common topic in the development world over the years. It’s a challenging part of the development process, especially for huge applications. This challenge in state management has been solved in many different ways over time and keeps evolving in positive ways from the grassroot React useReducer.
In this article, we’re going to learn from grassroots, what there is to know about the useReducer
hook, how it helps you manage application state better (and logic), along with real-world examples of how it works and why you might want to use it in your next or current project for better state management.
The React useReducer Syntax
const [state, dispatch] = useReducer(reducer, initialArg, init);
The React useReducer is a pure function that takes up to three arguments and returns a state and a dispatch.
These three arguments are used to determine what the state is and how the dispatch function works.
Don’t worry about understanding this upfront, we’ll go through every inch of what this means and how it works.
What is a state in React?
const [state, ...
A state in react is a piece of data that represents the current status of our application. This state can be used to provide information on the screen or perform background computations/calculations that allow our application to function. State is a fundamental idea in React.
Here’s a visual example of a state in React, holding some information about a user.
What is a dispatch in React?
const [state, dispatch...
Dispatch in React is simply a function that takes an instruction about what to do, then passes that instruction to the reducer as an “action”. In simple terms, see dispatch as a friendly boss that likes to tell the reducer what to do and is happy about it
If you’re familiar with Redux, this term dispatch might not be new to you but, we’ll go through this article assuming you’re new to both Redux and useReducer. First, reducer function.
The reducer function
const [state, dispatch] = useReducer(reducer, ...
The reducer function is a special handler that takes in two parameters. Reducer function takes in the application’s current state
object and an action
object. Then reducer function uses that to determine/compute the next state of our application (it returns a new state).
Remember how we talked about the dispatch
telling the reducer function what to do by passing it an action ?. These actions for reducer function usually contain a type
(what to do) and a payload
(what it needs to do the job).
Here’s a typical example of what a reducer function looks like:
1 2 3 4 5 6 7 8 9 10 11 12 | function reducer(state, action) { switch (action.type) { case "FIX_BUGS": return { totalBugs: state.totalBugs - 1 }; case "CREATE_BUGS": return { totalBugs: state.totalBugs + 1 }; case "SET_TOTAL_BUGS": return { totalBugs: action.payload }; default: throw new Error(); } } |
Now, after reducer function, we’re starting to connect the dots as we’ll go through what all these mean visually.
Let’s delve into what’s happening in the switch statement. In a real-world application, you’d most likely have more complex logic in the switch
but for this example, we’ll be keeping it simple.
We have three cases in our switch
statement, which are case "FIX_BUGS"
, case "CREATE_BUGS"
and case "SET_TOTAL_BUGS"
. These are the actions we’re explicitly trying to handle. The default
only fires when we dispatch
an action that doesn’t match any of our cases. See cases (or action types) as a store of all possible things a particular reducer can do. If the reducer
were a person, “cases” would be the skill list on the CV.
Here’s a visual representation of how the dispatch tells the reducer what to do.
The initial state
const [state, dispatch] = useReducer(reducer, initialArg, ...
The initial state in a useReducer
function is the starting state of our application e.g. The initial state or the default state. In the example we used earlier, we’re setting the total bugs of our application in the reducer function based on what type of action the dispatch
tells us. For this reason, we can set the starting point or default state of our bugs count to say… “0” ? or maybe even “100”? .
Let’s do a hundred! {totalBugs : 100}
Lazy initialization
const [state, dispatch] = useReducer(reducer, initialArg, init);
The useReducer
takes in an optional third parameter we’ll call init
. This init
is going to be a function we’ll pass as the third argument to useReducer
. This can be useful if we’d like to create the initial state lazily.
A common use case would be a situation where the initial state needs to go through some calculations to arrive at a default state or has to fetch data from an API, etc. The initial state looks something like this in code:
1 2 3 4 5 6 7 8 9 | const init = (initalState) => { console.log(initalState); // 100 //... do some code magic <img draggable="false" role="img" class="emoji" alt="" src="https://s.w.org/images/core/emoji/15.0.3/svg/1fa84.svg"> //... return inital; // return desired state }; |
How the React useReducer hook works
Now that we’ve gone through the syntax: const [state, dispatch] = useReducer(reducer, initialArg, init);
from left to right, it’s time to start putting together the pieces in code.
Here’s the complete code snippet for our bugs count application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | import { useReducer, useState } from "react"; function reducer(state, action) { switch (action.type) { case "FIX_BUGS": return { totalBugs: state.totalBugs - 1 }; case "CREATE_BUGS": return { totalBugs: state.totalBugs + 1 }; case "SET_TOTAL_BUGS": return { totalBugs: action.payload }; default: throw new Error(); } } const init = (inital) => { console.log(inital); // 100 return inital; }; export default function App() { const [state, dispatch] = useReducer(reducer, { totalBugs: 100 }, init); // creating a new state so we don't add extra in the reducer state // (this is just for examples) const [inputState, setInputState] = useState(0); return ( <div className="App"> <h1>useReducer</h1> <p>{state.totalBugs} Bugs Left <img draggable="false" role="img" class="emoji" alt="" src="https://s.w.org/images/core/emoji/15.0.3/svg/1f9d1-200d-1f4bb.svg"></p> <button onClick={() => dispatch({ type: "FIX_BUGS" })}>FIX_BUGS</button> <button onClick={() => dispatch({ type: "CREATE_BUGS" })}> CREATE_BUGS </button> <input onChange={(e) => setInputState(+e.target.value)} value={inputState} type="number" /> <button onClick={() => dispatch({ type: "SET_TOTAL_BUGS", payload: inputState }) } > SET_TOTAL_BUGS </button> </div> ); } |
For a live preview of the working code. I created a sandbox.
From the walkthrough of the useReducer syntax and what each bolt and knot means, to seeing the actual code in action. We can denote that the useReducer
is a function that can take up to three arguments and returns a state
and a dispatch
.
Here’s a simple and summarized visual representation of the useReducer
lifecycle in code.
Bailing out of a dispatch
If you return the same state
in your reducer
hook as the current state
. React is smart enough to bail out of rendering or firing effects on components depending on that state
. This is because React uses the Object.is comparison algorithm and this tells React that nothing has changed.
useReducer with Typescript
useReducer
is no stranger to us anymore but how do we actually use it with TypeScript ?. Now if you’re already familiar with TypeScript, this one should be a piece of cake, however, if you’re fairly new, don’t worry, we’ll have a quick and simple example to get us going.
What we’re going to do here is by no means a rule, you’re free to use any formula or paradigm that makes you sleep better at night .
Here it is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | import { ChangeEvent, useReducer } from "react"; // for our state interface State { firstName: string; lastName: string; age: number; language: string; } // list of all possible types enum ActionTypes { UPDATE = "UPDATE", RESET = "RESET" } // action type interface Actions { type: ActionTypes; payload?: { key: string; value: string; }; } // if we need to do some work to provide initial state const init = (inital: State) => { console.log(inital); return inital; }; // the default state of our app const initialState = { firstName: "", lastName: "", age: 0, language: "" }; // the reducer handler function const userFormReducer = (state: State, action: Actions) => { switch (action.type) { case "UPDATE": return { ...state, [action.payload.key]: action.payload.value }; case "RESET": return initialState; default: throw new Error(); } }; export const UserForm = () => { const [state, dispatch] = useReducer(userFormReducer, initialState, init); // for input change event const handleChange = (event: ChangeEvent<HTMLInputElement>) => { dispatch({ type: ActionTypes.UPDATE, payload: { key: event.target.name, value: event.target.value } }); }; return ( <div> <h1>TypeScript Example</h1> <input value={state.firstName} type="text" name="firstName" onChange={handleChange} placeholder="first name" /> <input value={state.lastName} type="text" name="lastName" onChange={handleChange} placeholder="lastName" /> <input value={state.language} type="text" name="language" onChange={handleChange} placeholder="language" /> <input value={state.age} type="text" name="age" onChange={handleChange} placeholder="" /> <button onClick={() => dispatch({ type: ActionTypes.RESET })} type="reset" > RESET </button> </div> ); }; |
To test this out yourself, live on Sandbox. I’ve also included the typescript example.
This article is not about TypeScript, so we won’t be explaining what’s going on in that code. A few things to note however is that I intentionally put all the TypeScript code in one file. In a real-world application, you’s most certainly create files for different things and your approach might be quite different (Again, whatever helps you sleep better at night ).
useState Vs useReducer
1 2 3 | const [state, dispatch] = useReducer(reducer, initialArg, init); //vs const [state, setState] = useState(initialState) |
We won’t be creating a webinar or zoom meeting to argue about this, but a rule of thumb is, that everything you can do with a useReducer
you can do with a useState
. In fact, the useReducer
hook is just an alternative to useState
with a few clear differences such as:
- Readability
- Ease of use and,
- Syntax
Note
React guarantees thatdispatch
function identity is stable and won’t change on “re-renders”. This is why it’s safe to omit from theuseEffect
oruseCallback
hook dependency list.
When to use the useReducer hook
use a useReducer
hook over useState
when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. This is fairly common when your application begins to grow in size. useReducer
also lets you improve performance for components that trigger deep updates because you can pass down a dispatch instead of callbacks.
When not to use React useReducer
useReducer
is awesome, but there are certain scenarios where it doesn’t make our lives any easier. Here are a few of such cases:
- Don’t use a
useReducer
hook when you’re dealing with simple state logic. - When your application needs a single source of truth. You’ll be better off using a more powerful library like Redux
- When prop-drilling starts to yell at you. This happens when you get trapped in the hellish world of passing down too many props (state) to and from child components to a child component that later comes to hunt you.
- When state lifting to parent component/top-level components no longer suffices.
Recommend videos
There’s always more to learn about state management, so if you’d like to learn more tricks and hacks about React hooks or additional hooks, I’d recommend you look up the video links below
Interesting reads from our blogs
- React SVG: How to use SVGs in React?
- React.js Components: A-Z Guide
- React.js Conditional Rendering – A Complete Guide
- Learn How to Make Drag and Drop with React Draggable
Conclusion
The useReducer is a powerful hook if used properly and I hope we’ve been able to learn about it without any confusion. It’s a great hook for state management as well. If you’d like to contribute to this post or drop feedback, feel free to leave a comment and drop a like . You can also check out CopyCat to convert Figma to React with the click of a button, We’ll speed up your sprints by eliminating sprint delays and making the handoffs painless.
Thanks for reading, you’re awesome!
Happy Coding!