useReducer: React Hooks

Harsh Mishra - Nov 8 - - Dev Community

useReducer in React: Simplify State Management with Two Mini-Projects

Introduction

State management is a critical part of building dynamic and interactive applications in React. While useState is sufficient for managing simple state, as your application's state grows in complexity, useReducer offers a more powerful, predictable way to handle it. Inspired by Redux's reducer pattern, useReducer allows you to define how state transitions should happen in response to specific actions, making it ideal for scenarios with multiple, complex state updates.

In this article, we’ll:

  1. Walk through a clear explanation of useReducer, its syntax, and when to use it.
  2. Implement two mini-projects:
    • Counter with Multiple Actions: An example that goes beyond basic increment/decrement, showing how useReducer handles multiple action types.
    • To-Do List with Complex State Transitions: A to-do app that highlights useReducer's ability to manage complex state objects.

Let’s dive into how useReducer can simplify your state management in React!


Understanding useReducer

What is useReducer?

useReducer is a React hook designed for situations where useState falls short. Instead of directly updating state, you specify a reducer function that calculates the next state based on the current state and an action. This declarative approach keeps state transitions predictable and allows you to manage more complex state logic in a centralized way.

Syntax of useReducer

Here’s a breakdown of the syntax:

const [state, dispatch] = useReducer(reducer, initialState);
Enter fullscreen mode Exit fullscreen mode
  • reducer: A function that defines how the state should be updated based on the action. It takes two arguments:

    • state: The current state.
    • action: An object with information about the action, typically including a type and an optional payload.
  • initialState: The starting value for the state.

Example: Basic Counter with useReducer

Let’s create a simple counter using useReducer to see the syntax in action.

import React, { useReducer } from 'react';

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        default:
            return state;
    }
}

function Counter() {
    const [state, dispatch] = useReducer(reducer, { count: 0 });

    return (
        <div>
            <p>Count: {state.count}</p>
            <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
        </div>
    );
}

export default Counter;
Enter fullscreen mode Exit fullscreen mode

Explanation of the Code

  1. Reducer Function: This function defines how to handle actions. Based on the action type (increment or decrement), the reducer function returns a new state object.
  2. Dispatching Actions: dispatch sends an action to the reducer, which processes it and updates the state accordingly.

When to Use useReducer

useReducer is especially useful when:

  • State logic is complex or involves multiple sub-values.
  • The next state depends on the previous state.
  • Multiple components need to access the state managed by the reducer (you can combine useReducer with useContext for global state).

Mini Project 1: Counter with Multiple Actions

In this project, we’ll create an enhanced counter that allows multiple operations (increment, decrement, reset) to see how useReducer handles a broader set of actions.

Step 1: Define the Reducer Function

import React, { useReducer } from 'react';

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        case 'reset':
            return { count: 0 };
        default:
            throw new Error(`Unknown action: ${action.type}`);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Counter Component

function EnhancedCounter() {
    const [state, dispatch] = useReducer(reducer, { count: 0 });

    return (
        <div>
            <p>Count: {state.count}</p>
            <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
            <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
        </div>
    );
}

export default EnhancedCounter;
Enter fullscreen mode Exit fullscreen mode

This enhanced counter now supports reset functionality in addition to increment and decrement. This project demonstrates useReducer’s flexibility in managing actions for state updates.


Mini Project 2: Building a To-Do List with Complex State Transitions

The to-do list app highlights how useReducer is ideal for managing complex state objects with multiple transitions, such as adding, removing, and toggling tasks.

Step 1: Define the Reducer

function todoReducer(state, action) {
    switch (action.type) {
        case 'add':
            return [...state, { id: Date.now(), text: action.payload, completed: false }];
        case 'remove':
            return state.filter(todo => todo.id !== action.payload);
        case 'toggle':
            return state.map(todo =>
                todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
            );
        default:
            throw new Error(`Unknown action type: ${action.type}`);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the To-Do List Component

import React, { useReducer, useState } from 'react';

function ToDoList() {
    const [todos, dispatch] = useReducer(todoReducer, []);
    const [task, setTask] = useState('');

    const handleAdd = () => {
        if (task.trim()) {
            dispatch({ type: 'add', payload: task });
            setTask(''); // Clear input field
        }
    };

    return (
        <div>
            <h2>To-Do List</h2>
            <input
                value={task}
                onChange={e => setTask(e.target.value)}
                placeholder="Enter a new task"
            />
            <button onClick={handleAdd}>Add Task</button>

            <ul>
                {todos.map(todo => (
                    <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
                        {todo.text}
                        <button onClick={() => dispatch({ type: 'toggle', payload: todo.id })}>
                            {todo.completed ? 'Undo' : 'Complete'}
                        </button>
                        <button onClick={() => dispatch({ type: 'remove', payload: todo.id })}>Delete</button>
                    </li>
                ))}
            </ul>
        </div>
    );
}

export default ToDoList;
Enter fullscreen mode Exit fullscreen mode

Explanation of the To-Do List Code

  1. Actions:

    • Add: Adds a new task to the list with a unique ID and completed status set to false.
    • Remove: Deletes a task by filtering it out based on the ID.
    • Toggle: Marks a task as completed or uncompleted by toggling the completed status.
  2. Using useReducer with Dynamic Data: This example shows how useReducer handles complex, nested state updates in an array of objects, making it perfect for managing items with multiple properties.


Conclusion

In this article, you’ve learned how to effectively use useReducer for more complex state management in React applications. Through our projects:

  1. The Enhanced Counter demonstrated how useReducer simplifies multiple action-based state updates.
  2. The To-Do List illustrated how to manage complex state objects, like arrays of tasks, with useReducer.

With useReducer, you can write cleaner, more predictable, and maintainable code for applications that require robust state management. Experiment with these projects, and consider useReducer next time you encounter complex state logic in your React apps!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .