When Should We Use useReducer Instead of useState, and What Exactly Is It?

·

6 min read

Managing state in React can be a complex task, especially when dealing with multiple sub-values or when the next state depends on the previous one. In this guide, we’ll explore the useReducer hook, a powerful tool in React that provides a more centralized way of handling such complex state updates. It’s similar to useState, but when should you use useReducer over useState? Let’s dive into this with some real-life analogies.

useState vs useReducer: Light Switch vs Thermostat

Think of useState as a simple light switch. It has two states: on and off. You can easily flip the switch to change the state. This is similar to how useState works in React. It's perfect for handling simple state changes, like toggling a boolean or incrementing a counter.

On the other hand, useReducer is like a thermostat. It's more complex than a light switch. You can turn it up, turn it down, set it to a specific temperature, switch between heating and cooling, etc. This is where useReducer shines. It's designed to handle more complex state logic that involves multiple sub-values or when the next state depends on the previous one.

So, what does this mean in terms of code? Let’s take a look at this now:

Understanding useReducer

useReducer is a React Hook that lets you add a reducer to your component. A reducer is a function that determines how the state should be updated in response to an action. It’s like the internal mechanism of a thermostat that decides how to adjust the temperature based on the current temperature and the desired temperature. The reducer function is where you define how the state should be updated in response to different actions. It should be a pure function, meaning it should not have side effects and its return value should only depend on its input values. For each action type, it returns a new state object with the updated values. Here’s the basic syntax:

const [state, dispatch] = useReducer(reducer, initialArg, init?)

To use useReducer, you need to call it at the top level of your component, just like how you would set a thermostat in a central location in your home. The dispatch function returned by useReducer allows you to update the state and trigger a re-render of the component. You pass an action to the dispatch function, and React will set the next state based on the return value of the reducer function. This is similar to how a thermostat receives a new temperature setting and adjusts the heating or cooling to achieve that temperature.

The reducer function is where you define how the state should be updated in response to different actions. For each action type, it returns a new state object with the updated values. This is like the thermostat deciding whether to turn up the heat, turn on the air conditioning, or do nothing based on the current and desired temperatures.

Here’s an example of a reducer function using the temperature analogy:

function thermostatReducer(state, action) {
  switch (action.type) {
    case 'SET_TEMPERATURE':
      return { ...state, desiredTemperature: action.temperature };
    case 'INCREMENT_TEMPERATURE':
      return { ...state, desiredTemperature: state.desiredTemperature + 1 };
    case 'DECREMENT_TEMPERATURE':
      return { ...state, desiredTemperature: state.desiredTemperature - 1 };
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

function ThermostatComponent() {
  const [state, dispatch] = useReducer(thermostatReducer, { desiredTemperature: 72 });
  // ...
}

In this example, the thermostatReducer function handles three types of actions: 'SET_TEMPERATURE', 'INCREMENT_TEMPERATURE', and 'DECREMENT_TEMPERATURE'. For each action type, it returns a new state object with the updated desiredTemperature. This is similar to how a thermostat adjusts its settings based on the temperature you set.

So, in terms of code, useReducer is a powerful tool that gives you more control over your state, similar to how a thermostat gives you more control over your home's temperature compared to a simple on/off switch.

Dispatching Actions

Dispatching actions with useReducer is a way to update your state and trigger a re-render of the component, similar to how interacting with the buttons on your thermostat allows you to control the temperature in your home. Here’s an example of how you might dispatch an action:

const [state, dispatch] = useReducer(thermostatReducer, { desiredTemperature: 72 });

function increaseTemperature() {
  dispatch({ type: 'INCREMENT_TEMPERATURE' });
}

In this example, when increaseTemperature is called, it dispatches an action of type ‘INCREMENT_TEMPERATURE’. The thermostatReducer function will then calculate the next state based on this action.

So, dispatching actions with useReducer is a way to update your state, similar to how interacting with the buttons on your thermostat allows you to control the temperature in your home.

Avoiding Recreating the Initial State

Consider this: you’ve set your thermostat to maintain a comfortable room temperature of 72 degrees Fahrenheit. Once this temperature is set, the thermostat remembers it and maintains that temperature until you decide to change it. It doesn’t need to calculate or “recreate” this temperature setting every time it checks the temperature; it simply knows it’s set to 72 degrees and acts accordingly.

In the context of React, the initial state is like the temperature you’ve set on your thermostat. Once set, React keeps this initial state in memory and uses it for every render. However, if you’re recalculating the initial state during every render, it’s like the thermostat forgetting its temperature setting and having to recalculate it constantly.

To avoid this unnecessary recalculation, you can use an initializer function with useReducer. This function calculates the initial state once, and React reuses this state in subsequent renders. This is similar to setting the temperature on your thermostat once and having the thermostat maintain that temperature without needing to recalculate it. Here's an example:

function calculateInitialState(currentTemperature) {
  // Let's say the expensive computation is to convert Fahrenheit to Celsius
  const initialTemperatureInCelsius = (currentTemperature - 32) * (5/9);
  return { temperature: initialTemperatureInCelsius };
}

function ThermostatComponent({ currentTemperature }) {
  const [state, dispatch] = useReducer(thermostatReducer, currentTemperature, calculateInitialState);
  // ...
}

In this example, calculateInitialState is the initializer function. It's like setting the temperature on your thermostat. It calculates the initial state once, and then React, like the thermostat, remembers this state and doesn't need to recalculate it for every render.

So, by using an initializer function with useReducer, you can ensure that React efficiently maintains the state of your components, just like a thermostat efficiently maintains the temperature of your room.

In conclusion, useReducer is a powerful tool for managing complex state logic in your React components. It's like a thermostat that gives you more control over your state compared to a simple light switch (useState). By understanding how to use it effectively, you can write cleaner and more maintainable code.

That’s it for my deep dive into useReducer. If you found this guide helpful, please consider sharing it with others who might also benefit. Don’t forget to follow me for more insightful content on React and other programming topics. Stay tuned for more, and happy coding!