Understanding useReducer Hook in React

The useReducer hook in React provides an alternative to useState for managing complex state logic in your applications. It is particularly useful when state transitions depend on previous states or when the state logic is too complex to handle with useState alone.

useReducer

When we creating more functions in our project that time length of code is increase and also code is hard to understand.

Simple use of useReducer with count app example.

Using of snippet create useReducer.

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

Now create reducer function that mentioned in useReducer hook.

const reducer = (state, action) => {
    switch (action.type) {
        case ACTIONS.INC:
            return { count: state.count + 1 }
        case ACTIONS.DEC:
            return { count: state.count - 1 }
        default:
            return state
    }
}

We use ACTIONS for never change value of keywords and when we create object vscode simply give hints.

const ACTIONS = {
    INC : 'increment',
    DEC : 'decrement'
}

Now we create a buttons to perform this counter app and header to display count.

<h1>{state.count}</h1>
<button onClick={() => dispatch({type: ACTIONS.INC})}>+</button>
<button onClick={() => dispatch({type: ACTIONS.DEC})}>-</button>

Now this is a complete useReducer how to write.

import { useReducer } from "react"

const ACTIONS = {
    INC : 'increment',
    DEC : 'decrement'
}

const reducer = (state, action) => {
    switch (action.type) {
        case ACTIONS.INC:
            return { count: state.count + 1 }
        case ACTIONS.DEC:
            return { count: state.count - 1 }
        default:
            return state
    }
}

const UseReducer = () => {
    const [state, dispatch] = useReducer(reducer, { count: 0 })
    return (<>
        <h1>{state.count}</h1>
        <button onClick={() => dispatch({type: ACTIONS.INC})}>+</button>
        <br />
        <button onClick={() => dispatch({type: ACTIONS.DEC})}>-</button>
    </>)
}

Todo App using useReducer hook

import { useState, useReducer } from "react"
const ACTIONS = {
    ADD_TODO: 'addTodo',
    TOGGLE_TODO: 'toggleTodo',
    DELETE_TODO: 'deleteTodo',
}

const reducer = (todos, action) => {
    switch (action.type) {
        case ACTIONS.ADD_TODO:
            return [...todos, newTodo(action.payload.nameofTask)]
        case ACTIONS.TOGGLE_TODO:
            return todos.map(todo => {
                if (todo.id === action.payload.id) {
                    return { ...todo, complete: !todo.complete }
                }
                else return todo
            })
        case ACTIONS.DELETE_TODO:
            return todos.filter(todo => todo.id !== action.payload.id)
        default:
            return todos
    }
}

const newTodo = (task) => {
    return { id: Date.now(), name: task, complete: false }
}

const UseReducer = () => {
    const [todos, dispatch] = useReducer(reducer, [])
    const [task, settask] = useState('')


    const handleSubmit = (e) => {
        e.preventDefault()
        task !== '' ? dispatch({ type: ACTIONS.ADD_TODO, payload: { nameofTask: task } }) : null
        settask('')
    }


    return (<>
        <form onSubmit={handleSubmit}>
            <input type="text" value={task} onChange={e => settask(e.target.value)} />
            <button type="submit">Add</button>
        </form>
        {todos.map((val) => {
            return (
                <div key={val.id}>
                    <h2 style={{ textDecoration: !val.complete ? 'none' : 'line-through' }}>{val.name}</h2>
                    <button onClick={() => dispatch({ type: ACTIONS.TOGGLE_TODO, payload: { id: val.id } })}>Toggle</button>
                    <button onClick={() => dispatch({ type: ACTIONS.DELETE_TODO, payload: { id: val.id } })}>Delete</button>
                </div>
            )
        })}
    </>)
}

export default UseReducer

Basic Example: Counter App

Let's start with a simple example of a counter app using useReducer.

import { useReducer } from "react";

const ACTIONS = {
    INC: 'increment',
    DEC: 'decrement'
};

const reducer = (state, action) => {
    switch (action.type) {
        case ACTIONS.INC:
            return { count: state.count + 1 };
        case ACTIONS.DEC:
            return { count: state.count - 1 };
        default:
            return state;
    }
};

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

    return (
        <>
            <h1>{state.count}</h1>
            <button onClick={() => dispatch({ type: ACTIONS.INC })}>+</button>
            <button onClick={() => dispatch({ type: ACTIONS.DEC })}>-</button>
        </>
    );
};

In this example, we define a reducer function that takes the current state and an action and returns the new state. We then use useReducer to create a state and a dispatch function, similar to how we use useState.

Info

Using useReducer can make your code more readable and maintainable, especially in scenarios where state transitions are complex or involve multiple actions.

Todo App using useReducer

Let's look at a more complex example: a Todo App using useReducer.

import { useState, useReducer } from "react";

const ACTIONS = {
    ADD_TODO: 'addTodo',
    TOGGLE_TODO: 'toggleTodo',
    DELETE_TODO: 'deleteTodo'
};

const reducer = (todos, action) => {
    switch (action.type) {
        case ACTIONS.ADD_TODO:
            return [...todos, newTodo(action.payload.nameofTask)];
        case ACTIONS.TOGGLE_TODO:
            return todos.map(todo => {
                if (todo.id === action.payload.id) {
                    return { ...todo, complete: !todo.complete };
                } else {
                    return todo;
                }
            });
        case ACTIONS.DELETE_TODO:
            return todos.filter(todo => todo.id !== action.payload.id);
        default:
            return todos;
    }
};

const newTodo = (task) => {
    return { id: Date.now(), name: task, complete: false };
};

const TodoApp = () => {
    const [todos, dispatch] = useReducer(reducer, []);
    const [task, setTask] = useState('');

    const handleSubmit = (e) => {
        e.preventDefault();
        task !== '' && dispatch({ type: ACTIONS.ADD_TODO, payload: { nameofTask: task } });
        setTask('');
    };

    return (
        <>
            <form onSubmit={handleSubmit}>
                <input type="text" value={task} onChange={e => setTask(e.target.value)} />
                <button type="submit">Add</button>
            </form>
            {todos.map((val) => (
                <div key={val.id}>
                    <h2 style={{ textDecoration: val.complete ? 'line-through' : 'none' }}>{val.name}</h2>
                    <button onClick={() => dispatch({ type: ACTIONS.TOGGLE_TODO, payload: { id: val.id } })}>Toggle</button>
                    <button onClick={() => dispatch({ type: ACTIONS.DELETE_TODO, payload: { id: val.id } })}>Delete</button>
                </div>
            ))}
        </>
    );
};

In this example, we use useReducer to manage the state of our Todo App. We define a set of actions and a reducer function that handles these actions to update the state.

Info

useReducer is a powerful tool for managing complex state logic in your applications. It provides a centralized way to handle state transitions and actions, making your code more organized and easier to maintain.

Task

Practice Using useReducer

Objective: Create a component that allows users to manage a list of items with add, toggle, and delete functionalities.

  1. Create a Todo Component:

    Create a new file Todo.jsx and define a component that includes a form for adding new items and a list to display existing items.

  2. Implement useReducer:

    Utilize the useReducer hook to manage the state of the todo list, including adding, toggling, and deleting items.

  3. Handle Form Submission:

    Implement a function to handle form submissions and add new items to the todo list.

  4. Render the Component:

    Import and render the Todo component in your application to test its functionality.