Managing React Complex State with the useReducer()

Managing React Complex State with the useReducer()

As your React application grows in size and complexity, so does the state that you need to manage in your components. Sometimes, this state can become so complicated that it can be difficult to manage with just the useState hook alone. This is where the useReducer hook comes in.

What is useReducer?

The useReducer hook is a built-in React hook that provides a way to manage a more complex state in your components. It works by allowing you to define a state object and an action object that can update that state.

Basic Syntax

The basic syntax for useReducer is as follows:

import { useReducer } from 'react';
function reducer(state, action) {
// logics to update states
//...

}
function MyComponent() {
  const [state, dispatch] = useReducer(reducer, object);
  // ...
}

Let's break this down:

  • state is the current state of your application

  • dispatch is a function that you can call to update the state

  • reducer is a function that takes the current state and an action and returns a new state based on that action

  • object/initialstate is the initial state of your application

Behind the Scene

How does useReducer work?

In the workflow outlined above, the state is initialized with an object. When we want to update the state, we use dispatch, which takes an object with different properties. In the example above, the object passed to dispatch includes type and id properties. The action object, which is the second argument of the reducer function, receives this object. The reducer function then returns the updated state based on the type property and any additional conditions. By using dispatch to update the state and the reducer function to determine how the state is updated, we can manage complex state more easily and with more predictable results.

So What's the Benefit?

While there may not be many benefits to using useReducer if you only have a few states to manage, it can be extremely helpful if you are dealing with more than five states. By bringing all states inside a single object, you can avoid the need for separate initialization and updating functions for each state. This also means that all the logic for updating the states is located in one place and is therefore easier to manage. Additionally, updating the state is made easier with the use of a single dispatch function, which simplifies the overall process.

Here's a complete example useReducer :

import { useReducer } from 'react';
const ACTION = {
  INCREMENT: 'increment',
  DECREMENT: 'decrement',
  NEW_USER_INPUT: 'newUserInput',
  TG_COLOR: 'tgColor'
}
const reducer = (state, action) => {
  switch (action.type) {
// Different Logics for different state at one place
    case ACTION.INCREMENT:
      return { ...state, count: state.count + 1 };
    case ACTION.DECREMENT:
      return { ...state, count: state.count - 1 };
    case ACTION.NEW_USER_INPUT:
      return { ...state, userInput: action.payload };
    case ACTION.TG_COLOR:
      return { ...state, color: !state.color };
    default:
      throw new Error();
  }
}
// All States in a sigle object
const Object={ count: 0, userInput: '', color: false }
function App() {
  const [state, dispatch] = useReducer(reducer, object )

  return (
    <main className="App" style={{ color: state.color ? '#FFF' : '#FFF952' }}>
      <input
        type="text"
        value={state.userInput}
        onChange={(e) => dispatch({ type: ACTION.NEW_USER_INPUT, payload: e.target.value })}
      />
      <br /><br />
      <p>{state.count}</p>
      <section>                // Just pass the type to action
        <button onClick={(() => dispatch({ type: ACTION.DECREMENT }))}>-        </button>
        <button onClick={(() => dispatch({ type: ACTION.INCREMENT }))}>+</button>
        <button onClick={(() => dispatch({ type: ACTION.TG_COLOR }))}>Color</button>
      </section>
      <br /><br />
      <p>{state.userInput}</p>
    </main>
  );
}
export default App;

Example Explained:

(Skip this if you understand the above code)

Firstly, an ACTION object is created, which is used to define the different actions that can be taken on the state. This is to avoid typing mistakes, usually, developers do this.

Next, a reducer function is defined, which takes in the current state and an action as parameters. The switch statement inside the reducer function handles the different cases for the various actions defined in the ACTION object. Each case returns a new state object with the appropriate updates applied.

The Object constant is used to define the initial state object, which contains the count, user input, and color states.

Finally, the App component uses useReducer to initialize the state object and dispatch actions to update the state. The state is passed as state and the dispatch function is passed as dispatch to the component.

In the component, the state properties are accessed using state.count, state.userInput, and state.color. The dispatch function is used to update the state based on user input and button clicks.

Why use useReducer?

There are several advantages to using useReducer over useState:

  • Simplifies complex state management: As mentioned earlier, useReducer comes in handy when there are more states in a single function and so many states have to pass down to the components. By defining a reducer function, you can simplify the logic for updating complex state and reduce the amount of boilerplate code that you need to write.

  • Predictable state updates: With useReducer, you can guarantee that state updates are predictable and follow a specific pattern. Because the reducer function is the only place where state updates can occur, it is easier to reason about the state of your application at any given time.

  • Performance optimization: When using useState, updating state triggers a re-render of the component. With useReducer, you have more control over when state updates trigger a re-render. This can be particularly useful in optimizing performance in larger applications.

Recommendation from React:

React recommends using useState initially, and when it is no longer sufficient for managing your state, useReducer can be used as an alternative. If your state management needs become even more complex, you may want to consider using other libraries such as Redux or React Query.

Conclusion

The useReducer hook is a powerful tool for managing complex state in your React components. By defining a reducer function, you can simplify the logic for updating complex state, guarantee predictable state updates, and optimize performance in larger applications. While it may take some time to get used to the syntax and pattern of using useReducer, it can be a valuable addition to your React toolkit.

Thank you for taking the time to read this blog. I hope it has been informative and has helped you gain a deeper understanding of the topic. If you have any feedback or suggestions for future content, please let me know!