Type to generate custom UI components with AI

Type to generate UI components from text

OR

Browse thousands of MUI, Tailwind, React components that are fully customizable and responsive.

Explore Components

An Easy Guide to the React useReducer Hook

In the world of React development, managing state is crucial for creating dynamic and interactive user interfaces. As React has grown, developers have adopted how they handle state within components. Let’s dive into the journey of state management in React, uncovering challenges from older methods and introducing a powerful solution – the `useReducer` hook.

The simplest and go-to way for managing stage in React revolved around using the `useState` hook along with other functions. While this is suitable for simpler applications, it became constraining as projects grew more intricate.

It was not specifically created to address any shortcomings of `useState`, but rather to handle more complex scenarios where state logic becomes more complicated.

Brief History of the React useReducer Hook?

`useReducer` was inspired by the concept of reducers in Redux, a popular state management library for React applications. Reducers are functions that take the current state value and an action as arguments and return a new state.

The primary motivation behind introducing `useReducer` was to have a more advanced tool compared to useState. It’s designed for situations where state transitions involve multiple steps, sub-states, or depend on the previous state. It also provides a structured way to handle these complex state updates and actions in your React components.

In a nutshell, React introduced `useReducer` to give developers a more powerful and flexible option for managing state, especially when dealing with more complex scenarios that go beyond the capabilities of the simpler `useState` hook.

What is the useReducer hook?

The useReducer hook makes handling complex state logic more straightforward. It provides a structured pattern for managing state transition and improving code organizations and readability.

const initialState = { count: 0 };

const reducer = (state, action) => { {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

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

As we’ve established earlier, the inspiration for the `useReducer` hook was from Redux. Where a simple function (the reducer) takes the current state and an action and returns the new state.

The `useReducer` hook brings predictability and scalability to state management. By adopting concepts from Redux, it lets developers manage state in a centralized way, making it easier to understand and maintain, even larger applications.

We will dive deeper into the code above in the next section and see how `useReducer` empowers developers to handle state more efficiently and neatly. If you’d like to go through a video to understand the `useReducer` hook better, then you probably might want to go through the video below:

PS: Engineers waste time building and styling components when working on a project, writing repetitive markup adds time to the project and is a mundane, boring task for engineers. PureCode.ai uses AI to generate a large selection of custom, styled UI components for different projects and component types.

Basics of useReducer

Let’s explore the fundamental concepts of the `useReducer` hook through a straightforward example—a simple counter app.

In this scenario, pressing a button triggers an increase in the count of a number, and the updated count is then displayed on the screen.

This simple illustration will help us grasp the basics of how the `useReducer` hook works clearly and understandably.

Understanding Syntax and Parameters in useReducer by Building a Counter App

Understanding how to use the `useReducer` hook involves grasping the syntax and the parameters it takes. But before that, we will demonstrate the simple counter app with a useState hook.

import React, { useState } from 'react';

function Counter() {
  // State variable to hold the count
  const [count, setCount] = useState(0);

  // Event handler for incrementing the count
  const handleIncrement = () => {
    setCount(count + 1);
  };

  // Event handler for decrementing the count
  const handleDecrement = () => {
    setCount(count - 1);
  };

  return (
    <div>
      <button onClick={handleDecrement}>Increment</button>
      <h2>Counter: {count}</h2>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );
};

export default Counter;

In the initial example, we crafted a straightforward counter app using the `useState` hook.

By destructuring `count` and `setCount` from the hook, we obtained the current state and the callback function to modify it. Here, `count` represents the actual state value, while `setCount` is the function that allows us to update this state.

Additionally, we implemented two extra functions linked to the `onClick` attribute of buttons. These functions utilized the `setCount` callback to handle incrementing and decrementing the count. As a result, the count increased or decreased by 1 with each button click.

Here’s what we have

Counter app using useState without styling

And when we add the styling below

.App {
  display: flex;
  gap: 10px;
  margin-left: 100px;
  margin-top: 100px;
  align-items: center;
}

span {
  font-weight: bold;
  font-size: 20px;
}

button {
  border: 0;
  padding: 10px 20px;
}

We get this image below which is better looking than what we had before.

Counter app using useState with styling

Using the useReducer Hook in the Counter App

Now, let’s explore how we can transform this app, primarily built with `useState`, into a version that leverages the `useReducer` hook.

import React, { useReducer } from "react";

const initialState = {
  count: 0
}

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

  return (
    <div className="App">
      <button>-</button>
      <span>{state.count}</span>
      <button>+</button>
    </div>
  );
}

export default Counter

After removing all instances of the `useState` hook from our code, we introduced the `useReducer` hook.

`useReducer` returns an array that contains both the state and a dispatch function. Although similar to the `useState` hook, there’s a small difference; `useReducer` accepts a reducer function and an initial state as arguments.

The initial state is typically structured as an object. In our example, `count` was initialized to `0`, `{ count: 0 }` which serves as a default value and loads the initial state lazily.

This approach also proves to be more effective in managing states, allowing for a cleaner organization of various state properties within the object.

In summary, the `useReducer` hook is a slightly different but powerful alternative to `useState`. It encourages the use of a reducer function and an initial state, often organized as an object, for more efficient state management.

The `reducer` Function

Before we go on here’s a video by Sam Meech-Ward that can help in your understanding of the reducer function.

Now let’s move on to define the reducer function;

import { useReducer } from "react";

const initialState = {
  count: 0
}

// The reducer function that takes state and action as parameters
const 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, initialState);

  return (
    <div className="App">
      <button>-</button>
      <span>{state.count}</span>
      <button>+</button>
    </div>
  );
}

export default Counter;

Now let us go over the reducer function line by line;

function reducer(state, action){

The reducer function takes in two parameters: `state` and `action`. The state represents the current state of our counter app, and `action` describes the type of change you want to make in that state.

switch (action.type) {

The switch statement is used to check the value of the `action.type`. The `type` property is a string that identifies the type of action being performed.

case 'increment':
  return { count: state.count + 1 };

If the `action.type` is equal to ‘increment’, the reducer returns a new state object where the `count` property is incremented by 1. This means it’s handling the action of incrementing the count.

case 'decrement':
  return { count: state.count - 1 };

If the `action.type` is equal to ‘decrement’, the reducer returns a new state object where the `count` property is decremented by 1. This means it’s handling the action of decrementing the count.

default:
  return state;

If the `action.type` does not match any of the specified cases (i.e., it’s not ‘increment’ or ‘decrement’), the reducer returns the current state unchanged. This is a common pattern for handling unknown action types gracefully.

The reducer function is a pure function and the central piece of logic that takes the current state and an action, and based on the action type, it produces a new state value.

This is a key concept in the use of the `useReducer` hook, where state transitions are determined when actions are dispatched, and the reducer is responsible for updating the state. Safe to say that the reducer function is the brain of any implementation with the `useReducer` hook.

Defining the `dispatch` function

Now that we have defined the reducer function we can go ahead to define our dispatch function.

import { useReducer } from "react";

const initialState = {
  count: 0
}

// The reducer function that takes state and action as parameters
const 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, initialState);

// functions that handle the dispatch of both increment and decrement actions
  const handleIncrement = () => {
    dispatch({ type: 'increment' });
  };

  const handleDecrement = () => {
    dispatch({ type: 'decrement' });
  };

  return (
    <div className="App">
      <button onClick={handleDecrement}>-</button>
      <span>{state.count}</span>
      <button onClick={handleIncrement}>+</button>
    </div>
  );
}

export default Counter;

In the code snippet above, we introduced two additional functions, `handleIncrement` and `handleDecrement`, which play a role in triggering the `dispatch` function. These functions are associated with buttons representing increment and decrement actions.

When the user clicks the button linked to `handleIncrement`, it signals the `dispatch` function to send an action of type ‘increment’ to the reducer. Similarly, the button associated with `handleDecrement` triggers the `dispatch` function to send an action of type ‘decrement’ to the reducer.

These functions are connected to specific buttons, and clicking each button invokes the corresponding function to communicate with the `dispatch` function, initiating the desired state changes through actions of ‘increment’ or ‘decrement’.

Furthermore, the `dispatch` function, provided by the `useReducer`, takes a specific action type. The action type is passed into the reducer function, which checks if it matches a particular case.

Depending on the case, the reducer function makes changes to the state. Importantly, when this change occurs, a new object reference is created for the state.

React, being aware of the change in object references, triggers a re-render of the component. In simple terms, we can think of the `dispatch` function as a messenger that receives an action type as a message and conveys the message to the reducer.

It helps manage state changes by specifying actions, and the associated reducer function determines how the state should be updated. The creation of a new object reference then signals React that a change has occurred, prompting a re-render of the component.

Refactoring the Counter App Code with useReducer Hook

Now let’s make a few adjustments to the code to make it more durable.

import { useReducer } from "react";

const initialState = {
  count: 0
}

// action types in an object
const ACTION = {
  INCREMENT: 'increment',
  DECREMENT: 'decrement'
}

// The reducer function that takes state and action as parameters
const reducer = (state, action) => {
  switch (action.type) {
    case ACTION.INCREMENT:
      return { count: state.count + 1 };
    case ACTION.DECREMENT:
      return { count: state.count - 1 };
    default:
      return state;
  }
};

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

// functions that handle the dispatch of both increment and decrement actions
  const handleIncrement = () => {
    dispatch({ type: ACTION.INCREMENT });
  };

  const handleDecrement = () => {
    dispatch({ type: ACTION.DECREMENT });
  };

  return (
    <div className="App">
      <button onClick={handleDecrement}>-</button>
      <span>{state.count}</span>
      <button onClick={handleIncrement}>+</button>
    </div>
  );
}

export default Counter;

In the provided code, we created an object named `ACTION` containing key-value pairs, specifically `INCREMENT` and `DECREMENT`.

Instead of using the actual strings ‘increment’ and ‘decrement’ directly in our functions, made use of the dot notation to reference these values from the `ACTION` object.

By doing this, our functions become more reusable and resistant to potential future changes. If we ever need to update the action types, we can do so within the `ACTION` object, ensuring consistency throughout the code without directly modifying the strings in each function. This approach enhances the maintainability of the code.

The output of the code snippets and explanation is attached below.

And please explore the CodeSandbox to review the entire codebase for reference

final result of the code

When to Use useReducer?

Knowing when to choose `useReducer` over `useState` is essential for effective state management. Let’s delve into scenarios where `useReducer` excels and situations where it proves more advantageous than `useState`.

Manage Complex State Transitions

If your component deals with complicated state changes, like complex logic or simultaneous updates of multiple state variables, `useReducer` offers a more organized and effective approach. It thrives in situations where state changes are interconnected and demand careful management.

// Example: Complex state transition with useReducer

function reducer(state, action) {
  switch (action.type) {
    case 'updateName':
      return { ...state, name: action.payload };
    case 'incrementAge':
      return { ...state, age: state.age + 1 };
    default:
      return state;
  }
}

If you also notice problems in your code, like using `useEffect` excessively for state-related tasks, it might be a sign that `useReducer` is a better choice. `useReducer` can be more suitable when your code starts showing signs of these issues.

PureCode.ai can cater for your code development process. It will save you valuable time and effort on a project by providing customized, and ready-to-use components which will allow you to prioritize more important and thought-intensive tasks to speed up the development of your user interface.

Complex State Logic

When you’re dealing with complex state logic, you need a strong solution.

Let’s see how `useReducer` helps in situations with many interconnected sub-states and complex application states.

In real-world scenarios, a component’s state might involve multiple sub-states. For example, managing the state of a form with multiple fields.

const initialState = {
  username: '',
  password: '',
  // ... other form fields
};

`useReducer` allows you to structure your state logic in a way that makes handling multiple sub-states more manageable. Each action in the reducer can update a specific part of the state.

function formReducer(state, action) {
  switch (action.type) {
    case 'updateUsername':
      return { ...state, username: action.payload };
    case 'updatePassword':
      return { ...state, password: action.payload };
    // ... other form-related actions
    default:
      return state;
  }
}

`useReducer` makes handling complex application states easier by giving you a structured method. The logic for each part of the state is neatly organized within the reducer, making your code easier to maintain.

Best Practices and Tips: Reducer Composition

When using `useReducer`, how you put together your reducers is crucial for keeping your code manageable, scalable, and of high quality.

Let’s explore some best practices and tips for composing reducers effectively.

Guidelines for Composing Reducers for Maintainability

  • Separation of Concerns in Reducer Functions

    To make your code easier to maintain, make sure each reducer focuses on a specific part of your application’s state. Each reducer should deal with a set of actions related to a particular feature or state section.

    // Example: Separating concerns in a user-related reducer
    function userReducer(state, action) {
      switch (action.type) {
        case 'updateName':
          return { ...state, name: action.payload };
        case 'updateEmail':
          return { ...state, email: action.payload };
        default:
          return state;
      }
    }
  • Strategies for Organizing and Structuring Reducers

    Keep your reducers well-organized by grouping related ones together. You might want to create separate files for each reducer or group them by feature. This helps maintain a clear and structured codebase.

    // Example: Organizing reducers in a file
    // reducers/userReducer.js
    export function userReducer(state, action) {
      // ... reducer logic
    }
    
    // reducers/productReducer.js
    export function productReducer(state, action) {
      // ... reducer logic
    }
  • Best Practices for Naming Conventions and Structuring Reducer Files

    When naming your reducers and their files, stick to a consistent convention. Choose names that reflect the part of the state they handle. This consistency promotes clarity and helps developers understand the purpose of each reducer.

    // Example: Naming conventions for reducers and files
    // userReducer.js
    export function userReducer(state, action) {
      // ... reducer logic
    }
    
    // productReducer.js
    export function productReducer(state, action) {
      // ... reducer logic
    }

Recap of Key Concepts Covered

In this comprehensive guide, we’ve explored the ins and outs of the `useReducer` hook in React, a powerful tool that helps developers manage state in a more structured and scalable manner. Let’s briefly recap the key concepts covered.

Summary of useReducer’s Benefits and Applications

  • Structured State Management: `useReducer` provides a structured pattern for managing state transitions, enhancing code organization and readability.

  • Scalability: The hook proves particularly valuable as applications grow in complexity, offering a scalable solution to handle intricate state logic.

Highlights from Real-World Examples and Best Practices

We delved into real-world examples, such as building a Todo application and refactoring existing code. These examples illustrated how useReducer can be applied in practical scenarios, emphasizing best practices for optimal results.

Also, thoughtful state management is the cornerstone of a well-architected React application. By carefully considering how the state is structured and updated, you contribute to a codebase that is not only functional but also easy to understand and maintain.

As you embark on your React journey, consider adopting `useReducer` as a fundamental tool in your state management arsenal. Embrace the shift towards more thoughtful and centralized state management, paving the way for scalable and maintainable React applications.

For the most accurate documentation on useReducer, it is highly recommended to refer directly to the official React docs.

Here are other videos that can help in understanding the useReducer hook 


Shadrach Abba

Shadrach Abba