Learn Computing from the Experts | The Rheinwerk Computing Blog

What Is the Reducer Hook in React?

Written by Rheinwerk Computing | Jul 26, 2024 1:00:00 PM

The reducer hook in React works like the state hook, with the difference that you do not manipulate the state directly via a setter function but use the dispatch function to dispatch

action objects.

 

You process the action objects in the reducer function and create a new state on this basis in React.

 

Quick Start

const [state, dispatch] = useReducer(state, reducer);

 

You can use the reducer hook as a replacement for the state hook. The listing below shows an implementation of the BooksList component that creates a local state via the useReducer function. The only operation allowed by this component is to evaluate the entries.

 

Import { useReducer } from 'react';

import produce from 'immer';

import { StarBorder, Star } from '@mui/icons-material';

 

function reducer(state, action) {

   switch (action.type) {

       case 'RATE':

           return produce(state, (draftState) => {

               const index = draftState.findIndex(

                   (book) => book.id === action.payload.id

               );

               draftState[index].rating = action.payload.rating;

           });

       default:

           return state;

   }

}

 

const initialBooks = [

   {

       id: 1,

       title: 'JavaScript - The Comprehensive Guide',

       author: 'Philip Ackermann',

       isbn: '978-3836286299',

       rating: 5,

   },

   …

];

 

function BooksList() {

   const [books, dispatch] = useReducer(reducer, initialBooks);

   return (

       <table>

           <thead>

               <tr>

                   <th>Title</th>

                   <th>Author</th>

                   <th>ISBN</th>

                 <th></th>

               </tr>

           </thead>

           <tbody>

               {books.map((book) => (

                   <tr key={book.id}>

                       <td>{book.title}</td>

                       <td>{book.author}</td>

                       <td>{book.isbn}</td>

                       <td>

                           {new Array(5).fill('').map((item, i) => (

                               <button

                                   className="ratingButton"

                                   key={i}

                                   onClick={() =>

                                    dispatch({

                                         type: 'RATE',

                                         payload: { id: book.id, rating: i + 1 },

                                     })

                                   }

                               >

                                  {book.rating < i + 1 ? <StarBorder /> : <Star />}

                              </button>

                           ))}

                       </td>

                   </tr>

              ))}

           </tbody>

       </table>

   );

}

 

export default BooksList;

 

The core of this component, which renders a list of books, is the call of the useReducer function. When calling it, you pass a reducer function and an initial state to it. As a return, you get an array with two elements. The first element provides read access to the state, similar to the state hook. The second element of the array is the dispatch function.

 

The Reducer Function

The reducer function is the eponymous element of the reducer hook. Like the setter function of the state, it is a function that gets the previous state as well as an action object. The action object describes the change to be made to the state. The core of the reducer function is a switch statement in which you decide to what branch you want to jump based on the type property of the action object. In the case of the RATE type, you use Immer to create a copy of the state and set the new rating for the specified record. Then you return the modified state as the value of the reducer function.

 

Actions and Dispatching

The second element that makes up the reducer hook is an action. Actions are simple JavaScript objects that describe a change to the state. There is a best practice that such an action object should have a type property as well as a payload property. You use the type property to decide which operation you want to perform in the reducer function. The payload property can have any data structure required to control the operation. In the case of the rating example, the payload property contains an object with the ID of the affected record and the new rating value.

 

What’s still missing is the connection between the action object and the reducer function. This is where the dispatch function comes into play. You get this function as the second element of the return value of the useReducer function. The dispatch function makes sure that the reducer function is executed along with the action object that has been passed in order to produce a new state and re-render the component. In the example, you call the dispatch function in the click handler of the rating buttons.

 

The rating example contains only one operation, and it is synchronous—for good reason. It isn’t possible to handle asynchronicity within the reducer function. In the next section, you’ll learn how to deal with asynchronous operations.

 

Asynchronicity in the Reducer Hook

You have several options to deal with asynchronicity:

  • The first and simplest option is to first perform the asynchronous operation (e.g., loading or writing data) and then dispatch a corresponding action. However, by doing so, you lose the advantage of the reducer hook, which allows you to pull operations out of your component and encapsulate them cleanly.
  • The second variant is to attach one or more effect hooks to your reducer hook, which respond to the corresponding state changes and encapsulate the asynchronous side effects. However, this variant makes the source code significantly more complex, and the dependencies between the state and the effect hooks are not necessarily always directly obvious.
  • The third and most elegant variant is that you create an additional wrapper function to wrap the reducer hook, handle the asynchronicity, and dispatch the action. The listing below shows the implementation of such a middleware option that allows for loading and evaluating data records with a server.

Due to the asynchronous middleware, this example is a bit more extensive and is divided into three parts: middleware function, reducer function, and the BooksList component. For a better understanding, let's look at the three parts separately. To test the code yourself, you’ll want to put the individual parts together in one file.

 

import { useReducer, useMemo, useEffect } from 'react';

import produce from 'immer';

import { StarBorder, Star } from '@mui/icons-material';

 

function middleware(dispatch) {

   return async function (action) {

       // eslint-disable-next-line default-case

       switch (action.type) {

          case 'FETCH':

               const fetchResponse = await fetch(

                   `${process.env.REACT_APP_API_SERVER}/books`

               );

               const books = await fetchResponse.json();

               dispatch({ type: 'LOAD_SUCCESS', payload: books });

               break;

           case 'RATE':

               await fetch(

                   `${process.env.REACT_APP_API_SERVER}/books/${action.payload.id}`,

                     {

                         method: 'PUT',

                         headers: { 'Content-Type': 'Application/JSON' },

                         body: JSON.stringify(action.payload),

                     }

                 );

                 dispatch({

                    type: 'RATE_SUCCESS',

                     payload: { id: action.payload.id

   rating:action.payload.rating },

                 });

                 break;

       }

   };

}

 

The middleware function accepts a reference to the dispatch function you get from the reducer hook as a return value as an argument, and in turn returns a function itself. This function assumes the role of the dispatch function and is called with an action object. The function body consists of a switch statement that decides what needs to be done based on the action passed. In the example, you implement cases for actions using the FETCH and RATE types. In the case of FETCH, you load the data for the display from the server interface and then execute the dispatch function with a new action object. This is of type FETCH_SUCCESS and contains the loaded data as payload. The action is then processed by the actual reducer function.

 

The second action supported by the middleware is of the RATE type. In this case, you perform an asynchronous write operation on the server. To do this, you use the PUT method and configure the request so that it has the correct content type and contains the modified record as the payload. If the server reports a success of the write operation, you dispatch a RATE_SUCCESS action, which in turn is further processed by the reducer function.

 

In the next step, you implement the reducer function that takes care of processing the FETCH_SUCCESS and RATE_SUCCESS actions. Listing 6.4 contains the source code of the function:

 

function reducer(state, action) {

   switch (action.type) {

       case 'LOAD_SUCCESS':

           return action.payload;

       case 'RATE_SUCCESS':

           return produce(state, (draftState) => {

               const index = draftState.findIndex(

                   (book) => book.id === action.payload.id

                );

               draftState[index].rating = action.payload.rating;

           });

       default:

           return state;

   }

}

 

As with previous implementations, this reducer function is completely synchronous in design. The middleware function triggers the reducer function by calling the dispatch function. In the case of the FETCH-SUCCESS action, you return the data of the action object stored in the payload property.

 

If your application triggers the RATE_SUCCESS action, you use the Immer library to create a copy of the previous state and change the rating of the affected record.

 

Using these two functions, you have done all the preparatory work and can move onto implementing the component. This component is responsible for the state, must trigger the loading of the data, and displays the list of books. This listing contains the implementation of the BooksList component:

 

function BooksList() {

   const [books, dispatch] = useReducer(reducer, []);

 

   const middlewareDispatch = useMemo(() => middleware(dispatch), [dispatch]);

 

   useEffect(() => {

       middlewareDispatch({ type: 'FETCH' });

   }, [middlewareDispatch]);

 

   return (

       <table>

           <thead>

               <tr>

                  <th>Title</th>

                   <th>Author</th>

                   <th>ISBN</th>

                   <th></th>

               </tr>

           </thead>

           <tbody>

              {books.map((book) => (

                  <tr key={book.id}>

                   <td>{book.title}</td>

                   <td>{book.author}</td>

                   <td>{book.isbn}</td>

                   <td>

                       {new Array(5).fill('').map((item, i) => (

                           <button

                               className="ratingButton"

                               key={i}

                             onClick={() =>

                                 middlewareDispatch({

                                     type: 'RATE',

                                     payload: { ...book, rating: i + 1 },

                                 })

                             }

                           >

                               {book.rating < i + 1 ? <StarBorder /> : <Star />}

                           </button>

                         ))}

                     </td>

                 </tr>

             ))}

         </tbody>

     </table>

   );

}

 

export default BooksList;

 

In the first step, you call the useReducer function to get access to the state and dispatch function. As the initial value, you pass an empty array. This results in the component not displaying any data at first. Then you call the middleware function and pass the dispatch function to it. At this point, you use the useMemo function that ensures the middleware function will not be recreated for each render operation.

 

Finally, using the effect hook, you make sure that the data for the display is loaded from the server. You call the middlewareDispatch function with a FETCH action object, which is first processed by the middleware and then sets the state via the reducer function, leading to the display of the data.

 

In the table, you represent the individual data records, and when a rating star is clicked on, you make sure that the RATE action will be processed by the middleware. Again, the middleware triggers the server communication and then the RATE_SUCCESS action, which then leads to the update of the representation via the reducer.

 

Editor’s note: This post has been adapted from a section of the book React: The Comprehensive Guide by Sebastian Springer.