DEV Community

Mohamed Idris
Mohamed Idris

Posted on

How to use the useReducer Hook in React

The useReducer hook is a powerful tool in React for managing complex state logic in components. It offers an alternative to useState when you need more control over state updates, especially when dealing with multiple state values or actions that manipulate your state in different ways.

When to Use useReducer

You should consider using useReducer over useState in the following scenarios:

  1. Complex state logic: If your component state involves multiple values or updates that depend on each other, useReducer helps you centralize the logic.
  2. Multiple actions: When you need to handle several different actions that affect the state, useReducer provides a more structured approach.
  3. State dependent on previous state: If state updates are dependent on the current state (like toggling between values), useReducer ensures consistency in handling such updates.

How useReducer Works

useReducer works by taking in two things:

  1. Reducer function: This function determines how the state should change based on the action dispatched.
  2. Initial state: The initial state can be an object, array, or any data structure you need.

useReducer returns:

  • State: The current state of the component.
  • Dispatch: A function used to send actions to the reducer to update the state.

Setting Up useReducer

  1. Define the Initial State: This can be an object with multiple properties. For example:
const defaultState = {
  people: data,  // data is an array of people
};
Enter fullscreen mode Exit fullscreen mode
  1. Create the Reducer Function: The reducer function takes two parameters: state and action. The action contains the type (the action type) and any additional data needed. Based on the action type, the reducer updates the state.
const reducer = (state, action) => {
  switch (action.type) {
    case 'REMOVE_ITEM':
      return {
        ...state,
        people: state.people.filter(person => person.id !== action.id),
      };
    case 'RESET_ITEMS':
      return {
        ...state,
        people: data, // reset to initial data
      };
    case 'CLEAR_ITEMS':
      return {
        ...state,
        people: [], // clears the list
      };
    default:
      return state;
  }
};
Enter fullscreen mode Exit fullscreen mode
  1. Use the useReducer Hook: Call useReducer, passing in the reducer function and initial state.
const [state, dispatch] = useReducer(reducer, defaultState);
Enter fullscreen mode Exit fullscreen mode
  1. Dispatching Actions: To modify the state, you dispatch an action. Each action should have a type and may include additional properties, such as an id to remove an item.
const removeItem = (id) => {
  dispatch({ type: 'REMOVE_ITEM', id });
};

const clearItems = () => {
  dispatch({ type: 'CLEAR_ITEMS' });
};

const resetItems = () => {
  dispatch({ type: 'RESET_ITEMS' });
};
Enter fullscreen mode Exit fullscreen mode

Example: A List of People

Let’s implement a list where users can remove items, clear the list, or reset it using useReducer.

import { useReducer } from 'react';
import { data } from '../../../data'; // initial data

const defaultState = {
  people: data,
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'REMOVE_ITEM':
      return {
        ...state,
        people: state.people.filter(person => person.id !== action.id),
      };
    case 'RESET_ITEMS':
      return {
        ...state,
        people: data,  // reset to original data
      };
    case 'CLEAR_ITEMS':
      return {
        ...state,
        people: [], // clear all items
      };
    default:
      return state;
  }
};

const App = () => {
  const [state, dispatch] = useReducer(reducer, defaultState);

  const removeItem = (id) => {
    dispatch({ type: 'REMOVE_ITEM', id });
  };

  const clearItems = () => {
    dispatch({ type: 'CLEAR_ITEMS' });
  };

  const resetItems = () => {
    dispatch({ type: 'RESET_ITEMS' });
  };

  return (
    <div>
      {state.people.map((person) => {
        const { id, name } = person;
        return (
          <div key={id} className="item">
            <h4>{name}</h4>
            <button onClick={() => removeItem(id)}>remove</button>
          </div>
        );
      })}

      {state.people.length ? (
        <button
          className="btn"
          style={{ marginTop: '2rem' }}
          onClick={clearItems}
        >
          clear items
        </button>
      ) : (
        <button
          className="btn"
          style={{ marginTop: '2rem' }}
          onClick={resetItems}
        >
          reset items
        </button>
      )}
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Key Points to Remember

  1. Reducer Function: This is a pure function that receives the current state and an action, and returns a new state.
  2. Action Type: Each action must have a type to identify the action and determine what to do with the state.
  3. Dispatch: The dispatch function sends actions to the reducer, which processes them and updates the state accordingly.
  4. State Update: Unlike useState, which directly updates the state, useReducer requires dispatching an action that triggers state updates in the reducer function.

When Not to Use useReducer

  • Simple State Management: If your component’s state is simple (for example, a boolean flag or a single number), useState is easier and more straightforward.
  • Low Complexity: If the state doesn’t need to change based on various actions, useState can be simpler to implement.

Conclusion

useReducer is a great tool for handling complex state logic, especially when managing multiple pieces of state or when updates depend on the previous state. It gives you more control over your state and helps organize your component logic when the state management grows in complexity.

Top comments (3)

Collapse
 
edriso profile image
Mohamed Idris

Action Naming Conventions in useReducer

In React, when using useReducer, it's a good practice to define action types as constants instead of writing them as strings directly. This helps prevent typos and makes your code more reliable.

Why Use Constants?

If you use strings directly, you might accidentally mistype the action type, leading to errors. By using constants, you can avoid this issue.

For example, instead of:

dispatch({ type: "clear list" });
Enter fullscreen mode Exit fullscreen mode

You can define a constant:

const CLEAR_LIST = "clear_list";
Enter fullscreen mode Exit fullscreen mode

Then, use the constant both when dispatching and checking the action type in the reducer:

dispatch({ type: CLEAR_LIST });
Enter fullscreen mode Exit fullscreen mode

And in the reducer:

if (action.type === CLEAR_LIST) {
  // logic
} else {
  throw new Error(`No matching action type: ${action.type}`);
}
Enter fullscreen mode Exit fullscreen mode

This makes it easier to spot typos, as the constant will be consistent throughout the code. If an invalid action is dispatched, the error will be clear, helping you debug faster.

Conclusion:

Using constants for action types prevents typos and makes your code cleaner and easier to debug.

Collapse
 
edriso profile image
Mohamed Idris

Passing Data to Actions with "Payload"

In useReducer, when you pass data to an action, it's common to include it inside a property called payload. This is a convention that makes it easier to handle data, even if you only need to pass one value like an id. By using payload, you can ensure consistency and clarity in how data is handled in the reducer.

Example:

When dispatching an action like REMOVE_ITEM, you pass the id inside a payload object:

const removeItem = (id) => {
  dispatch({ type: REMOVE_ITEM, payload: { id } });
};
Enter fullscreen mode Exit fullscreen mode

In the reducer, you access the id from action.payload.id:

if (action.type === REMOVE_ITEM) {
  let newPeople = state.people.filter(
    (person) => person.id !== action.payload.id
  );
  return { ...state, people: newPeople };
}
Enter fullscreen mode Exit fullscreen mode

This helps keep the action object consistent and organized, especially as your application grows and you need to pass more complex data. Even if you're just passing a single value like id, wrapping it in payload ensures that future changes or additional data can be added without disrupting the structure.

Conclusion:

Using payload to pass data in actions is a convention that helps keep the code clean and consistent. It makes it easy to scale and handle different types of data in actions, even if you're just passing a simple value like an id.

Collapse
 
edriso profile image
Mohamed Idris

Organizing Actions and Reducers in Separate Files

As your application grows and the number of actions increases, it's a good practice to split your actions and reducer into separate files to keep the code clean and manageable.

  1. Actions File (actions.js): Store all the action types as constants in a separate file. This way, you avoid hardcoding strings and make it easier to manage action types.

Example:

   export const CLEAR_LIST = 'CLEAR_LIST';
   export const RESET_LIST = 'RESET_LIST';
   export const REMOVE_ITEM = 'REMOVE_ITEM';
Enter fullscreen mode Exit fullscreen mode
  1. Reducer File (reducer.js): In this file, handle the state changes based on the action types imported from the actions file. This keeps your reducer focused solely on state management.

Example:

   import { CLEAR_LIST, RESET_LIST, REMOVE_ITEM } from './actions';

   const reducer = (state, action) => {
     if (action.type === CLEAR_LIST) {
       return { ...state, people: [] };
     }
     if (action.type === RESET_LIST) {
       return { ...state, people: data };
     }
     if (action.type === REMOVE_ITEM) {
       let newPeople = state.people.filter(person => person.id !== action.payload.id);
       return { ...state, people: newPeople };
     }
     throw new Error(`No matching "${action.type}" - action type`);
   };
Enter fullscreen mode Exit fullscreen mode
  1. Main Component: Import both the action types and the reducer into your main component file. This helps maintain clean separation between logic and UI, making your code more scalable.

Example:

   import { useReducer } from 'react';
   import { CLEAR_LIST, RESET_LIST, REMOVE_ITEM } from './actions';
   import reducer from './reducer';
Enter fullscreen mode Exit fullscreen mode

By following this structure, you ensure that your code remains organized and easier to maintain as it grows. This is especially useful when managing a large number of actions in your app.