In a previous post, I talked about how to use Reducers in vanilla JavaScript and HTML projects. In that post, you learned that while the reducers are pure functions that you can use with State Management libraries such as, you can still use Reducers in other types of projects that do not use Redux
. In this post, I am going to tell you how to use Reducers and the useReducer
hook for state management in React.
Table of Contents
What is a Reducer: A quick Recap
Do you remember what Reducers are? Let me Refresh your memory.
Reducers are functions used in programming to process and transform data. They are a fundamental concept in functional programming and are often associated with state management in web applications.
Reducers typically have two key characteristics:
- Deterministic: A reducer always produces the same output for the same set of input arguments. This means that if you call a reducer with specific inputs, it will consistently return the same result.
- No Side Effects: Reducers do not have any side effects, meaning they do not modify the global state, perform I/O operations, or make network requests while executing. They focus solely on processing data.
All right, I think that is sufficient for this post. If you need more explanation, please read my post on Reducers.
Reducers in React ( without Redux or React Context)
You can employ Reducers within React independently of the need to integrate state management libraries like Redux or without relying on React Context. The main purpose of having a reducer function in React is to manage and update the application’s state in a predictable and centralized manner. The reducers serve as the heart of your state management system in React.
We will develop a demo application to understand how you can use Reducers to manage the state of your application.
Let me tell you what we are going to create. This app will show you a list of Products. You can add, delete, and update his list. We are not going to use any database. I will use only a JSON file to keep the initial list of Products.
Setting up your project
- I used Vite.js to set up this( run npm create vite@latest ). You can use a similar tool such as create-react-app
- If you use Vite.js, select React as your framework and JavaScript as your language( variant ).
Adding the Reducer
As mentioned before, the heart of this app is the Reducer. Therefore, first, we are going to see how the productReducer.js looks like.
export default function productReducer( state , action ) {
switch( action.type ){
case 'add_product': return [...state, action.payload ]
case 'remove_product':
return state.filter( product => product.id !== action.payload.id );
case 'update_product':
return state.map(product => (
product.id === action.payload.id) ?
// If the product ID matches the updated product's ID, update it
{ ...product, ...action.payload }
:
// Otherwise, leave the product unchanged
product
);
default:
return state
}
}
The productReducer
function is responsible for managing the state of a list of products. It takes two parameters: state, which represents the current state (an array of products), and action, which contains information about what operation to perform (e.g., adding, removing, or updating a product).
- When the action type is ‘add_product,’ it adds a new product to the existing list by creating a new array with all the previous products (spread operator …state) and appending the new product (action.payload).
- When the action type is ‘remove_product,’ it removes a product from the list. It uses the filter method to create a new array that includes only the products whose IDs don’t match the ID of the product to be removed.
- When the action type is ‘update_product,’ it updates an existing product. It uses the map method to iterate over the products. If it finds a product with a matching ID, it creates a new object by merging the existing product’s properties with the updated properties ({ …product, …action.payload }). If there’s no match, it leaves the product unchanged.
- For any other action type, it returns the current state as is.
React’s useReducer hook
The App.jsx
is responsible for generating the main UI and dispatching app in this application. The most important thing in App.jsx
is the useReducer
hook. What is the syntax of the useReducer
hook ?
useReducer hook syntax
You can use React’s useReducer
hook to add a reducer function to another component in your application. The useReducer
hook takes a minimum of two required arguments.
useReducer( productReducer, ProductInventory );
Input parameters
The useReducer
hook in React takes two main parameters:
Reducer Function:
- This is your reducer function that specifies how the state should change in response to different actions.
- The reducer function must take two arguments: the current state and an action, and then return the new state based on these inputs.
- The reducer function we used is the
productReducer
.
Initial State:
- This is the initial value of the state before any actions are applied. It represents the starting point for your state management.
- We use
ProductInventory
as the initial state, which is a list of product names coming fromproduct.json
. I used only the names of the products to keep things simple.
Returns
The useReducer hook returns an array containing two elements:
const[ products , dispatch ] = useReducer( productReducer, ProductInventory );
State: The first element is the current state value, which you can read and display in your component. The first value, products, represents the current state ( of the application ) managed by the reducer.
Note: [products, dispatch
]: This is an array destructuring assignment. It’s used to extract two values from the result of calling the useReducer
hook.
Dispatch function: The second element is a dispatch function, which you can use to dispatch actions to the reducer. When you call this function with an action, it triggers the reducer to process the action and update the state accordingly. the second value, dispatch, is a function used to dispatch actions to the reducer( productReducer
).
Dispatching actions
Let’s take a look at how you should dispatch actions. For example, when you need to add a new product, all you need to do is pass your action object to the dispatch function as an argument.
Adding a new product
dispatch({
type: "add_product",
payload:{
"id": 4,
"name": "Product 4",
}
})
The action is an object with type and payload properties. The dispatch function wills will pass this action object to the productReducer
.
Similarly, you can pass other actions for removing and updating products in the list
Removing a product
dispatch( {
type: "remove_product",
payload:{ "id": 1 }
})
Updating a product
dispatch({
type: "update_product",
payload: {
"id": 4,
"name": "Product 5"
}})
Note: In the application, I dynamically assign values to the “id” and “name” of the payload. Therefore, you can see variables used to hold the values for these properties of the payload.
ProductList Component
There is one child complement used by the App.jsx
. I thought it is worth mentioning it. This component generates a list of Radio buttons. I pass the product object( which are elements of the products array in the current state ).
Using ProductList component in App.jsx to create a list of products
{
products.map( (product ) => <div key={ product.id }>
<ProductList
product={ product }
checked={selectedOption === product.id }
onChange={ ()=>handleOptionChange( product.id )}
name="product"
/>
</div>)
}
ProductList.jsx
function ProductList( { product ,checked, onChange }) {
return (
<label>
<input
name="product"
type="radio"
value={ product.id }
checked={checked}
onChange={onChange}
/>
{ product.name }
</label>
);
}
Conclusion
In React, Reducers are versatile tools that help manage and update application state efficiently. They can be used independently without relying on libraries like Redux or React Context, thanks to the useReducer
hook. The main purpose of using a reducer is to handle state changes in a predictable and controlled manner, making it easier to manage complex application logic and maintain a clear flow of data.