useReducer
hook is an alternative to the useState
hook, useReducer
helps you manage complex state logic in React applications.
When combined with other hooks like useContext
, useReducer
can be a good alternative to Redux. In certain cases, it is an outright better option.
In this tutorial, we will explore the useReducer
hook in depth, reviewing the scenarios in which you should and shouldn’t use it.
Let’s get started!
What is useReducer Hook?
useState
hook is not the only hook to manage state, useReducer
is also used for the same.
useReducer
provides us with a mechanism to change a state based on the rules we provide, taking an initial state as input. That is simply useState
dressed up.
useReducer
hook is better than useState
hook when we want to manage complex component states.
Syntax of useReducer Hook
The useReducer
hook accepts 2 arguments: the reducer function and the initial state. The hook then returns an array of 2 items: the current state and the dispatch function.
import { useReducer } from 'react'; function MyComponent() { const [state, dispatch] = useReducer(reducer, initialState); const action = { type: 'ActionType' }; return ( <button onClick={() => dispatch(action)}> Click me </button> ); }
Don’t worry about understanding this upfront, we will go through every inch of what this means and how it works.
useReducer Hook Syntax Breakdown
Now, let’s decipher what the terms of initial state, action object, dispatch, and reducer mean.
Initial State
initialState
defines the initial state of the component. For example, in the case of a counter state, the initial value is:
// initial state const initialState = { counter: 0 };
Let’s take one more example of initialState
which contains a list of dogs up for adoption with their name, breed, and adoption status.
const initialState = [ { name: "Waffles", breed: "Chihuahua", adopted: false, }, { name: "Charlie", breed: "Pitbull", adopted: true, }, { name: "Prince", breed: "German Shepherd", adopted: false, }, ];
Action Object
An action object is an object that describes how to update the state.
Typically, the action object has a property type
— a string describing what kind of state update the reducer must do.
For example, an action object to increase the counter can look as follows:
const action = { type: 'increase' };
If the action object must carry some useful information (aka payload) to be used by the reducer, then you can add additional properties to the action object.
For example, here is an action object meant to add a new user to the users state array:
const action = { type: 'add', user: { name: 'Sunil Pradhan', email: 'skp@mail.com' } };
user
is a property that holds the information about the user to add.
The action object is interpreted by the reducer function (described below).
Dispatch Function
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.
The dispatch function is created for you by the useReducer
hook:
const [state, dispatch] = useReducer(reducer, initialState);
So, dispatching means a request to update the state.
If you are familiar with Redux, this term dispatch might not be new to you.
Reducer Function
Reducer is a function that provides instructions on how to manage state. It takes two parameters state
and action
and it returns a new state.
// reducer type (state, action) => newState
In other words, the reducer is a pure function that accepts 2 parameters: the current state and an action object.
Depending on the action object, the reducer function must update the state in an immutable manner, and return the new state.
The following reducer function supports the increase and decrease of a counter state:
function reducer(state, action) { let newState; switch (action.type) { case 'increase': newState = { counter: state.counter + 1 }; break; case 'decrease': newState = { counter: state.counter - 1 }; break; default: throw new Error(); } return newState; }
The reducer above doesn’t modify directly the current state in the state variable, but rather creates a new state object stored in newState
, then returns it.
The function which contains all your state updates is called the reducer. This is because you are reducing the state logic into a separate function. The method you call to perform the operations is the dispatch method.
React checks the difference between the new and the current state to determine whether the state has been updated. So do not mutate the current state directly.
If all these terms sound too abstract, no worries! Let’s see how useReducer
works in an interesting example.
Example 1 – Classic Counter
In the below code example we have used useReducer
hook to implement classic counter functionality.
useReducer
hook in the below code accepts two arguments: the reducer function and the initial state value.
SimpleCounter.jsx
import { useReducer } from 'react'; const initialState = 0; function reducer(state, action) { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: throw new Error(); } } const SimpleCounter = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( <> <h1> Counter: {state} <br /> <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button> <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button> </h1> </> ); }; export default SimpleCounter;
App.jsx
import SimpleCounter from './SimpleCounter'; function App() { return ( <div> <SimpleCounter /> </div> ); } export default App;
The initial state is an integer value, which starts from 0. The reducer function takes in two arguments: the current state and action where the action is of string type in this example.
Note that we have used JSX in SimpleCounter.jsx to return the counter component having two buttons (+ and -) to increase and decrease the counter value.
On clicking these buttons in real-time, the dispatch function will be called which is usually used to change or replace the component’s state value.
We have also passed the action of type string i.e, type: 'INCREMENT'
OR type: 'DECREMENT'
in the dispatch function.
As we can observe in the output, we can dynamically click these increment and decrement buttons to increase and decrease the counter state value respectively.
Output:
Example 2 – Form Object
In the below code example, we have used useReducer
hook to implement a form object with name and age as form fields.
useReducer
hook in the below code accepts two arguments: the reducer function and the initial state value object.
FormObject.jsx
import { useReducer } from 'react'; function reducer(state, action) { switch (action.type) { case 'increment': { return { name: state.name, age: state.age + 1, }; } case 'decrement': { return { name: state.name, age: state.age - 1, }; } case 'change': { return { name: action.Name, age: state.age, }; } } throw Error(action.type); } const FormObject = () => { const [state, dispatch] = useReducer(reducer, { name: 'Sunil', age: 21 }); return ( <> <input value={state.name} onChange={(e) => dispatch({ type: 'change', Name: e.target.value })} /> <button onClick={() => dispatch({ type: 'increment' })}> Increment age </button> <button onClick={() => dispatch({ type: 'decrement' })}> Decrement age </button> <p> Age of {state.name} is {state.age} </p> </> ); }; export default FormObject;
App.jsx
import SimpleCounter from './FormObject'; function App() { return ( <div> <FormObject /> </div> ); } export default App;
An initial state is an object with the name ‘Sunil’ and age as ’21’. The reducer function takes in two arguments: the current state and action where the action is of string type in this example.
Note that we have used JSX in FormObject.jsx to return the component. The component consists of an input box and increment & decrement age buttons.
On clicking these buttons in real-time, the dispatch function will be called which is usually used to change or replace the component’s state.
We have also passed an action of type string i.e, type: 'increment'
OR type:'decrement'
OR type:'change'
in the dispatch function.
Note that what we type in the input box will be treated as a target value and it will be passed in the action of type: 'change'
.
The action is passed to the reducer function as an argument where we have used switch statements for different action-type cases to change/replace and return the new component’s state value.
Output:
As we can observe in the output, we can dynamically click these increment and decrement buttons to increase and decrease the age state value respectively.
Also what we type in the input box, will be reflected as the name whose age is specified.
Ok, now let’s get to the next example and make this.
Example 3 – Add to Cart
On clicking Add to Cart button, items should be added in the cart, and Items in Cart and Total Price should increase. However, on clicking Remove from Cart button, the opposite should happen.
AddToCart.jsx
import { useReducer } from 'react'; const itemsInCart = [ { id: 1, name: 'Butter Chicken', price: 200, }, { id: 2, name: 'Naan', price: 40, }, { id: 3, name: 'Jalebi', price: 20, }, { id: 4, name: 'Gulab Jamun', price: 10, }, ]; const reducer = (state, action) => { switch (action.type) { case 'ADD_TO_CART': return { ...state, totalItems: state.totalItems + 1, totalPrice: state.totalPrice + action.payload, }; case 'REMOVE_FROM_CART': return { ...state, totalItems: state.totalItems - 1, totalPrice: state.totalPrice - action.payload, }; default: return { ...state }; } }; const AddToCart = () => { const [state, dispatch] = useReducer(reducer, { totalItems: 0, totalPrice: 0, }); return ( <div> <h2>Items in Cart: {state.totalItems}</h2> <h2>Total Price: {state.totalPrice}</h2> <hr /> {itemsInCart.map(({ name, price }) => ( <div> <h3> Product: {name} || Price: {price} </h3> <button onClick={() => dispatch({ type: 'ADD_TO_CART', payload: price })} > Add to Cart </button> <button onClick={() => dispatch({ type: 'REMOVE_FROM_CART', payload: price }) } > Remove from Cart </button> </div> ))} </div> ); }; export default AddToCart;
App.jsx
import AddToCart from './AddToCart'; function App() { return ( <div> <AddToCart /> </div> ); } export default App;
Output:
Example 4 – Fetching Data with useReducer
Let’s say you are fetching some data, and you want to display:
- loading… while it’s fetching
- the data once you have it
- or an error if there is one
You will want all three of these to be in sync with each other. If you get the data, you want to make sure it’s not loading and there’s no error. If you get an error, it’s not loading and there’s no data.
This is a good use case for useReducer
hook.
DataFetch.jsx
import React, { useReducer, useEffect } from 'react'; import axios from 'axios'; const initialState = { loading: true, error: '', post: {}, }; const reducer = (state, action) => { switch (action.type) { case 'FETCH_SUCCESS': return { loading: false, post: action.payload, error: '', }; case 'FETCH_ERROR': return { loading: false, post: {}, error: 'Something went wrong!', }; default: return state; } }; const DataFetch = () => { const [state, dispatch] = useReducer(reducer, initialState); useEffect(() => { axios .get('https://jsonplaceholder.typicode.com/posts/1') .then((response) => { dispatch({ type: 'FETCH_SUCCESS', payload: response.data }); }) .catch((error) => { dispatch({ type: 'FETCH_ERROR' }); }); }, []); return ( <div> {state.loading ? 'Loading' : state.post.title} {state.error ? error : null} </div> ); }; export default DataFetch;
App.jsx
import DataFetch from './DataFetch'; function App() { return ( <div> <DataFetch /> </div> ); } export default App;
Here we have used useEffect hook and axios library along with useReducer hook.
useReducer Hook vs useState Hook
useReducer
and useState
both return a stateful value (state
), and a function to update the state (setState
and dispatch
).
In addition, both hooks receive an initial state value (initialValue
).
The main difference in these two initializations is that useReducer
also takes a reducer function, which will be called when we use the returned dispatch
function.
// useState implementation const [state, setState] = useState(initialState); // useReducer implementation const [state, dispatch] = useReducer(reducer, initialState);
Conclusion
The useState
hook is implemented using useReducer
hook. It means that useReducer
is primitive, and you can use it for everything that you can do with useState
.
useReducer
hook is extremely useful when working on complex and different states depend on each other.useReducer
is very similar to Redux if you want to not use a 3rd-party library or if it’s only for a component or two.
Hope this article helped you to understand useReducer
hook what is exactly.
Further Reading
Similar articles you may like
- useMemo Hook : Performance Optimization Hook For React App
- React Components – A Theoretical View
- JSX in React – For Absolute Beginners
- A Complete Beginner’s Guide to React Router
Add comment