Understanding React useReducer

Ayako yk - Dec 4 '23 - - Dev Community

In the previous blog post, I talked about useContext, where props can be accessed without prop drilling. However, as an app becomes more complex, it becomes challenging to manage all the states. Consequently, useReducer is often employed in conjunction with useContext.

useReducer is one of the React Hooks. Although I've used other Hooks for projects, I've never used useReducer, to be honest. Before delving into the usage of useReducer and useContext, I'd like to talk about what useReducer is.

Syntax

const [state, dispatch] = useReducer(reducer, initialArg, init?)
Enter fullscreen mode Exit fullscreen mode

useReducer is a state management hook, similar to useState. It takes three parameters: reducer (a function that specifies how the state gets updated), initialArg, and an optional init (an initializer function that returns the initial state. If it's not provided, the initial state is set to initialArg). The useReducer hook returns an array containing state and dispatch (a function).

Here's an example code from React's official documentation.
If you're familiar with Redux, it might be easier to understand to integrate the idea by reading the code below.
react.dev

import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}

const initialState = { name: 'Taylor', age: 42 };

export default function Form() {
  const [state, dispatch] = useReducer(reducer, initialState);

  function handleButtonClick() {
    dispatch({ type: 'incremented_age' });
  }

  function handleInputChange(e) {
    dispatch({
      type: 'changed_name',
      nextName: e.target.value
    }); 
  }

  return (
    <>
      <input
        value={state.name}
        onChange={handleInputChange}
      />
      <button onClick={handleButtonClick}>
        Increment age
      </button>
      <p>Hello, {state.name}. You are {state.age}.</p>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Image description

This is a form where the age is incremented when a user clicks on the "Increment age" button, and the name is displayed as a user enters it in the form.

Now, let's break this down.

Reducer Function

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}
Enter fullscreen mode Exit fullscreen mode

The reducer function takes in state and action, representing the current state and the type of action the user has "chosen," respectively. It returns the next state depending on the action type, following the convention of using switch for readability.
In this example, when the "Increment age" button is clicked, and incremented_age is called, the reducer function returns this object: { name: state.name, age: state.age + 1 };

According to the reference, it's recommended to wrap each case block in { and } curly braces to prevent variables declared inside different cases from clashing with each other.

Additionally, it's important to note that state is immutable, meaning we should avoid modifying any objects or arrays within it. To achieve this, a spread operator is used to create a shallow copy.

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // Return a new object
      return {
        ...state, // a spread operator
        age: state.age + 1
      };
    }
Enter fullscreen mode Exit fullscreen mode

useReducer()

const initialState = { name: 'Taylor', age: 42 };

export default function Form() {
  const [state, dispatch] = useReducer(reducer, initialState);

OR

  const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });
Enter fullscreen mode Exit fullscreen mode

state: the current state with the initial value of { name: 'Taylor', age: 42 }
dispatch: a function called depending on the user's action

When a user enters a new name:

function handleInputChange(e) {
  dispatch({
    type: 'changed_name',
    nextName: e.target.value
  });
}

<input
  value={state.name}
  onChange={handleInputChange}
/>
Enter fullscreen mode Exit fullscreen mode

When a user clicks on the button:

function handleButtonClick() {
  dispatch({ type: 'incremented_age' });
}

<button onClick={handleButtonClick}>
  Increment age
</button>
Enter fullscreen mode Exit fullscreen mode

React will:

  1. Store the next state
  2. Render your component with it
  3. Update the UI

Whether we should use the useState or the useReducer is discussed here:
Comparing useState and useReducer

This blog focuses on the usage of useReducer and useContext, so I'll skip that discussion.

Now that I understand the useReducer, it's time to explore why using both useReducer and useContext can be dynamic.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player