Table of content
Introduction
The useReducer Syntax
2.1. What is a state in React
2.2. What is a dispatch in React
2.3. The Reducer function
2.4. Initial state
2.5. Lazy initializationHow the useReducer hook works
Bailing out of a dispatch
useReducer with Typescript
useState Vs useReducer
When to use useReducer
When not to use useReducer
Recommend videos
Interesting reads
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 has been solved in many different ways over time and keeps evolving in positive ways.
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.
The useReducer Syntax
const [state, dispatch] = useReducer(reducer, initialArg, init);
The useReducer is a 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.
The reducer function
const [state, dispatch] = useReducer(reducer, ...
The reducer function is a special handler that takes in two parameters. The application's current state
and an action
object, then it 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 what to do by passing it an action π?. These actions 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:
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 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 react useReducer
function is the starting state of our application e.g. 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. It looks something like this in code:
const init = (initalState) => {
console.log(initalState); // 100
//... do some code magic πͺ
//...
return inital; // return desired state
};
How the 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.
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 π§βπ»</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:
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'd 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
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 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/top-level components no longer suffices.
Recommend videos
There's always more to learn, so if you'd like to learn more tricks and hacks about the useReducer hook. I'd recommend you look up the video links below
More interesting reads from my blogs
- How To Create A Global "JSON Search Algorithm" In JavaScript
- How To Know If An Element Is Visible In Viewport
- How To Create A "Glassmorphic" Template In Pure HTML & CSS
Conclusion
The useReducer is a powerful hook if used properly and I hope we've been able to learn about it without any confusion. If you'd like to contribute to this post or drop feedback, feel free to leave a comment π and drop a like β£οΈ. Thanks for reading, you're awesome! π
Happy Coding!
I'd love to connect with you π
PS: This article was initially published on "Copycat"
Top comments (0)