Let's dive into the power of the useReducer Hook!

Having so many useState really decreases the code readability..👨‍💻

·

9 min read

The React useReducer hook is a very good alternative to useState when you need to manage complex states with multiple values. In this tutorial, you will learn about this React hook. You will learn about how useReducer hook works. You will also learn how to use it to manage state.

A quick introduction to React useReducer hook:

The React useReducer hook is a lot like the useState hook. It helps you manage the state of your React applications, just like useState does. The cool thing about useReducer is that it's great for handling more complicated states. When I say complicated states, I mean states that have many sub-values, like an object with key-value pairs. The useReducer hook simplifies this by using a more structured approach.But that doesn't mean you can only use the useReducer hook for complex states. You can totally use it for simple states too, where you only have a single basic value.

So, here's how the useReducer hook works. It needs two things: the state itself and a function called a reducer. The reducer is a special kind of function that takes the current state and an action. The action tells the reducer what you want it to do, like incrementing a number, decrementing a number, or adding a new value to an array.

The reducer function takes these inputs, performs the action you specified, and gives you a new state value. This new state is an updated version of the state you provided. It's important to remember that the reducer should not directly modify the old state. It always returns a completely new state.

That's pretty much how the useReducer hook works. It's a simple and powerful tool for managing state in your React applications.

The syntax of useReducer hook:

The React useReducer hook takes three things as inputs. The first two are really important, while the third one is optional.

The first thing it needs is the reducer function we talked about earlier. This function is responsible for updating the state based on actions.

The second thing is the initial state value. It's just like what you're used to with the useState hook. It's the starting point for your state.

Now, let's talk about the optional third thing. It's called the initializer. This is a function that you can use to lazily initialize the state. In simple words, you can use this function to generate the initial state value, especially if it's a bit complicated or takes some time to create. But remember, React will only run this function once, during the initial render, not every time the component re-renders. So you might not need it as often.

When you use the useReducer hook, it gives you two things in return. First, it gives you the current state, which you can use to know the current value of your state. And second, it gives you a special function called dispatch. This function is really cool because it lets you update the state that you provided to the useReducer hook. It's like giving you the power to change the state whenever you need to.

So that's how the useReducer hook works in a nutshell. It takes the reducer function, the initial state, and an optional initializer, and gives you the current state along with a dispatch function to update the state.

// useReducer hook syntax:
const [state, dispatch] = useReducer(reducer, initialState, init)

Initial state

Before you can start using the useReducer hook you need two things, initial state and reducer function. Let’s start with the initial state. Initial state can be anything from primitive data type to object. Whatever fits your current situation. What you have to do is to create this state somewhere, and assign it to a variable.

// A simple initial state object:
const initialState = {
  name: '',
  email: '',
  role: '',
  isActive: false,
}

Reducer function

The second thing is the reducer function. The reducer function accepts two parameters: the state and action. It takes these two and updates the state, based on the action dispatched. It is very common to create the structure of reducer function, and handle each action, with switch statement.

The main reason is that switch is usually more readable than if...else statement. Especially when you work with multiple actions. That said, if you prefer if...else statement go ahead and use that. About the structure. The reducer has to have a case, or if block, for each action you want to use to update the state.

Each of these actions should do two things. First, it should copy the current state. Reducer is a pure function. It is not supposed to change the existing state. What it does instead is it creates copies of it and works with them. It is common to create copies of old state by spreading the old, using spread.

The second thing reducer will do for each case, or block, is updating specific state value with the new value. Put together, it will basically copy the old state and overwrite only the values that should be updated. After that, it will return the new state. Aside to this there should be also a default case or else block.

This case or block can do two things. First, it can return the original state unchanged. Second, it can throw an error about non-existing action. Similarly to initial state, you define the reducer as a function somewhere in your code. Don’t pass it to the reducer as a whole.

// Create reducer function:
const reducer = (state, action) => {
  // Create switch to handle all actions:
  switch (action.type) {
    case 'SET_NAME':
      // Handle 'SET_NAME' action:
      return {
        ...state, // Copy the old state.
        name: action.payload // Update relevant value.
      }
    case 'SET_EMAIL':
      // Handle 'SET_EMAIL' action:
      return {
        ...state, // Copy the old state.
        email: action.payload // Update relevant value.
      }
    case 'SET_ROLE':
      // Handle 'SET_ROLE' action:
      return {
        ...state, // Copy the old state.
        role: action.payload // Update relevant value.
      }
    case 'SET_IS_ACTIVE':
      // Handle 'SET_IS_ACTIVE' action:
      return {
        ...state, // Copy the old state.
        isActive: action.payload // Update relevant value.
      }
    default:
      // Throw an error when none of cases matches the action.
      throw new Error('Unexpected action')
  }
}

Action, type and payload

In the example of the reducer function, you might have noticed something called action.type and action.payload. When you use the dispatch function provided by the useReducer hook to update the state, you pass in an object. This object has two parts, one is called type, and the other is called payload. The type part tells the reducer function what kind of action you want to perform.

The reducer function then uses this information, the type, to decide what to do using switch cases or if blocks. The payload part is where you put the new value that you want the state to have. Remember, the names "type" and "payload" are not mandatory. They are just commonly used by React developers. You can choose any names you like for these parts. Just make sure you use the correct names in your reducer function.

// Dispatched object example to set name:
dispatch({
  type: 'SET_NAME',
  payload: 'Victor'
})

// Dispatched object example to set role:
dispatch({
  type: 'SET_ROLE',
  payload: 'Admin'
})

// Dispatched object example to set isActive:
dispatch({
  type: 'SET_IS_ACTIVE',
  payload: true
})

Putting it all together

  1. Use the useReducer hook to handle state management for you by providing the initial state and reducer function as arguments. This hook will give you the state and dispatch function.

  2. When you want to update a specific state value, use the dispatch function. Pass in an object with two keys, type and payload (or any names you prefer). The type must match one of the cases in your reducer function.

  3. The payload holds the new value you want to update the state with. It represents the value you want to store in the state.

  4. The state value returned by the useReducer hook always provides the latest state values, similar to the useState hook. The state remains the same, and the dispatch function acts as the updater for the state.

// Import useReducer hook from React:
import { useReducer } from 'react'

// Create initial state:
const initialState = {
  name: '',
  email: '',
  role: '',
  isActive: false,
}

// Create reducer function:
const reducer = (state, action) => {
  // Create switch to handle all actions:
  switch (action.type) {
    case 'SET_NAME':
      // Handle 'SET_NAME' action:
      return {
        ...state, // Copy the old state.
        name: action.payload // Update relevant value.
      }
    case 'SET_EMAIL':
      // Handle 'SET_EMAIL' action:
      return {
        ...state, // Copy the old state.
        email: action.payload // Update relevant value.
      }
    case 'SET_ROLE':
      // Handle 'SET_ROLE' action:
      return {
        ...state, // Copy the old state.
        role: action.payload // Update relevant value.
      }
    case 'SET_IS_ACTIVE':
      // Handle 'SET_IS_ACTIVE' action:
      return {
        ...state, // Copy the old state.
        isActive: action.payload // Update relevant value.
      }
    default:
      // Throw an error when none of cases matches the action.
      throw new Error('Unexpected action')
  }
}

// Create simple component:
export default function App() {
  // Call useReducer hook, passing in
  // previously created reducer function
  // and initial state:
  const [state, dispatch] = useReducer(reducer, initialState)

  return (
    <div className="App">
      {/*
        Create input for "name" and use dispatch
        to update "name" state value on input change.
      */}
      <input
        type="text"
        name="name"
        value={state.name}
        onChange={(event) => dispatch({
          type: 'SET_NAME', // Dispatch 'SET_NAME' action.
          payload: event.target.value // Set input value as payload.
        })}
      />

      {/*
        Create input for "email" and use dispatch
        to update "email" state value on input change.
      */}
      <input
        type="email"
        name="email"
        value={state.email}
        onChange={(event) => dispatch({
          type: 'SET_EMAIL', // Dispatch 'SET_EMAIL' action.
          payload: event.target.value // Set input value as payload.
        })}
      />

      {/*
        Create select for selecting "role" and use dispatch
        to update "role" state value on select change.
      */}
      <select
        onChange={(event) => dispatch({
          type: 'SET_ROLE', // Dispatch 'SET_ROLE' action.
          payload: event.target.value // Set input value as payload.
        })}
      >
        <option value="" selected></option>
        <option value="Admin">Admin</option>
        <option value="User">User</option>
        <option value="guest">Guest</option>
      </select>

      {/*
        Create checkbox for isActive and use dispatch
        to update "isActive" state value on checkbox change.
      */}
      <label>
        <input
          type="checkbox"
          checked={state.isActive}
          onChange={(event, checked) => dispatch({
            type: 'SET_IS_ACTIVE', // Dispatch 'SET_IS_ACTIVE' action.
            payload: checked // Set checkbox checked value as payload.
          })}
        />
        Is active?
      </label>
    </div>
  )
}

Conclusion: An Introduction to React useReducer hook:

The React useReducer hook is a good alternative to useState hook. Where useReducer can be very useful is when you have to deal with complex states. In these situations, useReducer might be a better choice than useState. I hope that this tutorial helped you understand how the React useReducer hook works and how to use it.

Â