React Design Pattern Part 4: State Reducer

Md Sajjad Hosen Noyon

20 October, 2024

For the state reducer pattern, we have to use a powerful hook, useReducer. This hook is a better alternative to the useState hook. When your component or application state management becomes complex, the reducer state pattern is a savior. The useReducer hook helps manage complex state management logic in React applications. Also, combining useContext and the useReducer hook can be a good alternative to Redux, Recoil, or MobX. In some instances, it is an outright better option. We can use this pattern for global state management.

When to use
  • It is used when more advanced state management is needed, especially in applications with complex state logic.
  • Using it has the benefit of predictability and traceability of state updates.
  • When you want to centralize state update logic in one place.
When not to use

Despite being able to use the useReducer hook to handle complex state logic in our app, it’s important to note that there are some scenarios where a third-party state management library like Redux may be a better option.

  • State management is simple in small or straightforward applications, and a more direct approach is sufficient.
  • When your application needs a single source of truth.
  • When state-lifting to the top-level component no longer suffices.
  • When you need to persist state.
Advantages
  • Improves application maintainability by centralizing state update logic.
  • Facilitates debugging and tracking of state changes by having a single place where updates occur.
  • Promotes a more scalable and structured design, especially in applications with a large amount of state logic.
Disadvantages
  • Increased boilerplate and verbosity.
  • Steeper learning curve.
  • Potential performance issues.
  • Complexity in managing large-scale applications.
  • Debugging challenges.
  • Overhead for simple use cases.
  • Synchronous nature requires additional tools for side effects.
Example

Now, it’s time to examine the Reducer state pattern and how it operates. We’ll implement a to-do application with a useReducer hook.

  • What is useReducer?

A useReducer hook in React allows you to add a reducer to your component. This hook takes a reducer function and an initialState as arguments. It also returns an array of the current state and a dispatch function.

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

Let’s familiarize ourselves with what the parameters mean:

  • state: It represents the current value and is set to the initialState value during the initial render.
  • dispatch: This function updates the state value and always triggers a re-render, just like the updater function in useState.
  • Reducer: This function houses all the logic for updating the state. It takes state and action as arguments and returns the next state.
  • initialState: It houses the initial value and can be any type.
				
					const initialState = {
 todos: []
}
				
			
Implementation of Reducer function

The implementation in the code snippet below concerns the reducer function, which is always declared outside of components. It takes two parameters: state(current state) and action (an action is a plain object that typically has a type property that identifies a specific action. Actions describe what happens and contain information necessary for the reducer to update the state). There is a switch case that determines which action has to be executed.

				
					// Reducing function to handle actions and update state
const reducer = (state, action) => {
 switch (action.type) {
   case 'ADD_TODO':
     return {
       ...state,
       todos: [
         ...state.todos,
         {
           id: state.todos.length + 1,
           text: action.payload,
           completed: false,
         },
       ],
     };
   case 'TOGGLE_TODO':
     return {
       ...state,
       todos: state.todos.map(todo =>
         todo.id === action.payload
           ? { ...todo, completed: !todo.completed }
           : todo
       ),
     };
   case 'REMOVE_TODO':
     return {
       ...state,
       todos: state.todos.filter(todo => todo.id !== action.payload),
     };
   default:
     return state;
 }
};
				
			

The reducer function has 3 cases with a few logic separately. All three cases are todos, adding, toggling, and removing.

How does the dispatch function work?

The dispatch function is returned by the useReducer hook and is responsible for updating the state to a new state. The dispatch function takes the only action as its argument. We can place the dispatch function inside an event handler function. In the code snippet below, three handlers that dispatch one action individually. Every action has an object with a mandatory type and payload property.

				
					const addTodo = (text) => {
   dispatch({ type: 'ADD_TODO', payload: text });
 };

 const toggleTodo = (id) => {
   dispatch({ type: 'TOGGLE_TODO', payload: id });
 };

 const removeTodo = (id) => {
   dispatch({ type: 'REMOVE_TODO', payload: id });
 };
				
			
What happens behind the hood?

Clicking the button triggers a dispatch function that sends type information to the reducer function. The dispatching (clicking of the button) causes the component to be re-render. The reducer function conditionally matches the case with the type from the action object and updates the state accordingly after evaluation has taken place.

 

The entire logic of our to-do app has been done. Now, we can return our JSX with the state to be displayed on the user interface, and the handler functions passed on the onClick event handler for the buttons.

Now, have a look at the JSX code.
				
					// Initial state
const initialState = {
 todos: []
}

// Example component using the State Reducer Pattern
const TodoList = () => {
 // Initialize the useReducer hook to manage state with the defined reducer
 .....

 // 3 Handler functions to handle user interactions
 .....

 // JSX code
 return (
   <div>
     <h2>Todo List</h2>
     <ul>
       {state.todos.map(todo => (
         <li key={todo.id}>
           <span
             style={{
               textDecoration: todo.completed ? 'line-through' : 'none',
             }}
             onClick={() => toggleTodo(todo.id)}
           >
             {todo.text}
           </span>
           <button onClick={() => removeTodo(todo.id)}>Remove</button>
         </li>
       ))}
     </ul>
     <input
       type="text"
       placeholder="Add todo..."
       onKeyDown={e => {
         if (e.key === 'Enter' && e.currentTarget.value.trim() !== '') {
           addTodo(e.currentTarget.value.trim());
           e.currentTarget.value = '';
         }
       }}
     />
   </div>
 );
};

export default TodoList;
				
			
Conclusion

We have covered the State Reducer Pattern, its advantages and disadvantages, and how the useReducer hook operates with a whole to-do app.

Thanks for reading.

Reference
Md Sajjad Hosen Noyon

20 October, 2024