useReducer – React

In React, the useReducer hook is an alternative to useState for managing more complex state logic. It is particularly useful when the state depends on previous state values, or when the state logic involves multiple sub-values, as is the case with more complex objects or arrays. It can be compared to how you would use a reducer in Redux, but on a local component level.

Basic Syntax of useReducer

The useReducer hook takes in two arguments:

  1. A reducer function that contains the state logic.
  2. An initial state value.

It returns an array with two elements:

  • The current state.
  • A dispatch function to trigger state updates.
const [state, dispatch] = useReducer(reducer, initialState);

The Reducer Function

The reducer function is responsible for determining how the state should be updated based on the action passed to it. It accepts two arguments:

  1. state: The current state.
  2. action: An object containing information (such as type and payload) about how the state should be updated.
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

Example 1: Simple Counter with useReducer

In this example, we use useReducer to manage the state of a counter.

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:
      throw new Error('Unknown action');
  }
}

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

  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;

Explanation:

  • The reducer function manages the state updates based on the action.type.
  • The dispatch function is used to send actions to the reducer (e.g., dispatch({ type: 'increment' })).
  • state.count is rendered, and we can increment or decrement it via buttons.

Example 2: Managing Form State with useReducer

In this example, we manage a form’s state with useReducer. It handles multiple fields and updates them using a single reducer function.

import React, { useReducer } from 'react';

const initialState = {
  username: '',
  email: '',
};

function formReducer(state, action) {
  switch (action.type) {
    case 'SET_FIELD':
      return {
        ...state,
        [action.field]: action.value,
      };
    case 'RESET_FORM':
      return initialState;
    default:
      return state;
  }
}

function UserForm() {
  const [state, dispatch] = useReducer(formReducer, initialState);

  const handleChange = (e) => {
    dispatch({
      type: 'SET_FIELD',
      field: e.target.name,
      value: e.target.value,
    });
  };

  const handleReset = () => {
    dispatch({ type: 'RESET_FORM' });
  };

  return (
    <div>
      <form>
        <div>
          <label>Username:</label>
          <input
            type="text"
            name="username"
            value={state.username}
            onChange={handleChange}
          />
        </div>
        <div>
          <label>Email:</label>
          <input
            type="email"
            name="email"
            value={state.email}
            onChange={handleChange}
          />
        </div>
        <button type="button" onClick={handleReset}>Reset</button>
      </form>
      <p>Username: {state.username}</p>
      <p>Email: {state.email}</p>
    </div>
  );
}

export default UserForm;

Explanation:

  • We store multiple form fields (username and email) in the state.
  • The reducer handles updates by looking at the action.field and updating the corresponding value (action.value).
  • The form input elements trigger the handleChange method, which dispatches an action to update the state.
  • A reset button calls dispatch({ type: 'RESET_FORM' }) to reset the form back to the initial state.

When to Use useReducer Over useState

While useState is often more straightforward, useReducer is preferred when:

  1. Complex state logic: If the state logic involves multiple sub-values or is deeply nested.
  2. State transition logic: When state transitions depend on the previous state.
  3. Multiple actions: When many types of actions need to be handled (e.g., form handling with multiple fields).
  4. Non-trivial state updates: If the updates to the state are non-trivial and would involve complex state manipulation.

Example 3: Complex State Management

If your state involves multiple values and actions, useReducer becomes more readable and manageable compared to multiple useState calls.

import React, { useReducer } from 'react';

const initialState = { loading: false, error: null, data: [] };

function dataFetchReducer(state, action) {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
        loading: true,
        error: null,
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        loading: false,
        data: action.payload,
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
        loading: false,
        error: action.error,
      };
    default:
      throw new Error();
  }
}

function DataFetchingComponent() {
  const [state, dispatch] = useReducer(dataFetchReducer, initialState);

  const fetchData = async () => {
    dispatch({ type: 'FETCH_INIT' });

    try {
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      dispatch({ type: 'FETCH_SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'FETCH_FAILURE', error: error.message });
    }
  };

  return (
    <div>
      {state.loading ? <p>Loading...</p> : <ul>{state.data.map(item => <li key={item.id}>{item.name}</li>)}</ul>}
      {state.error && <p>Error: {state.error}</p>}
      <button onClick={fetchData}>Fetch Data</button>
    </div>
  );
}

export default DataFetchingComponent;

Explanation:

  • This example manages loading, error, and data states for data fetching.
  • Different actions (FETCH_INIT, FETCH_SUCCESS, FETCH_FAILURE) control how the state is updated during the data-fetching process.

Summary

  • useReducer is ideal for managing complex state logic or state transitions.
  • It works well for scenarios where you have multiple actions or depend on the previous state for updates.
  • The pattern is similar to managing state in Redux, making it scalable for components with more complex logic.
Tags: No tags

Add a Comment

Your email address will not be published. Required fields are marked *