DEV Community

Cover image for useState and useReducer which should we use?
Jyoti chaudhary
Jyoti chaudhary

Posted on

useState and useReducer which should we use?

When it comes to state management in React, two commonly used hooks are useState and useReducer. These hooks play a crucial role in managing state within functional components. The choice between useState and useReducer in React depends on the complexity of your state management and how the state interacts within your component. Both hooks allow you to manage state, but they serve different purposes that developers should be aware of.. Here's a deeper dive into both hooks, comparing when to use each one and why.

1. useState Hook:

useState is the simplest hook to manage state in functional components. It's suitable when you only need to manage simple or isolated pieces of state.

Syntax:

const [state, setState] = useState(initialState);
Enter fullscreen mode Exit fullscreen mode
When to Use:
  • Simple state: When your component has small, isolated pieces of state that don't have complicated logic or interactions.
  • One state change: If your state can be described with one value or a simple array/object, then useState is easy to use and direct.
  • Local state management: It's great for form fields, toggling UI elements, or small flags.
Example:
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, useState is ideal because the state is a single number (count) and it's not dependent on any complex logic or actions.

Use Cases for useState:
  • Simple state variables:

When the state is a simple value (like a number, string, or boolean), useState is the easiest and most intuitive choice.
Example: Toggling a modal, showing or hiding UI elements, updating form fields.

Example:
const [isVisible, setIsVisible] = useState(false);
Enter fullscreen mode Exit fullscreen mode
  • Form Inputs:

useState is ideal for handling individual form fields or a set of fields where each field has its own independent state.
Example: Handling a user’s name and email input in a form.

Example:
const [name, setName] = useState('');
const [email, setEmail] = useState('');
Enter fullscreen mode Exit fullscreen mode
  • Single values with minimal logic:

If you need to keep track of a single piece of state like a counter, a boolean flag, or a single input field, useState provides an easy, straightforward solution.

Example:
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
Enter fullscreen mode Exit fullscreen mode
  • Local component state:

When the state is specific to a single component and doesn’t need to be shared across components, useState is perfect.

Limitations of useState:
  • Scaling issues with multiple state variables: As a component grows in complexity, managing multiple pieces of state with individual useState calls can become cumbersome and harder to maintain.
Example:

If you have a complex form with many fields, you’ll end up with several useState hooks, which can lead to messy code and potential bugs.

  • Difficulties with dependent states: If your state updates depend on previous values of the state, it can be tricky. Using a functional update inside setState is necessary, but can still get confusing when dealing with multiple dependencies.
Example:
setCount(prevCount => prevCount + 1);
Enter fullscreen mode Exit fullscreen mode
  • No centralized state logic:

There’s no central place to manage complex state transitions. Each useState hook operates independently, making state management harder when actions depend on each other.

2. useReducer Hook:

useReducer is more powerful than useState and is usually preferred for managing more complex state logic, especially when the state depends on multiple actions or needs to be updated based on previous state values.

Syntax:
const [state, dispatch] = useReducer(reducer, initialState);
Enter fullscreen mode Exit fullscreen mode
  • reducer: A function that determines how the state should change based on the dispatched action.
  • initialState: The initial value of the state.
When to Use:
  • Complex state logic: If the state has multiple sub-values, or when state transitions depend on multiple actions, useReducer can be a cleaner solution.
  • State transitions based on actions: If you need to manage a state object or array that gets updated based on a series of actions (like increment, decrement, reset, etc.), useReducer is a better choice because it centralizes the state logic.
  • Avoiding too many useState hooks: If you have a component with a lot of state variables or complex interactions between them, using multiple useState can get hard to manage. useReducer can help centralize and clean up the state logic.
Example:
import React, { useReducer } from 'react';

const initialState = { count: 0 };

function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, initialState);

  return (
    <div>
      <h1>{state.count}</h1>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this example, useReducer is beneficial because the state consists of an object ({ count: 0 }) and the state updates depend on multiple actions (increment, decrement, reset). The counterReducer function determines how to update the state based on the action type, making the logic more centralized and easier to manage.

Use Cases for useReducer:

  • Complex state logic:

When the state changes based on multiple actions or conditions, using useReducer centralizes the state management and makes it more organized.
Example: A complex form with multiple fields, each with its own validation state and error messages.

Example:
const initialState = { count: 0, status: 'idle' };
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'reset':
      return { ...state, count: 0 };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode
  • State transitions involving multiple actions:

When state changes involve multiple steps or actions, useReducer allows for more predictable transitions, especially in components where the state changes are complex.
Example: A component that handles a user’s authentication state (login, logout, session expired).

Example:
const initialState = { user: null, loading: false, error: null };
function authReducer(state, action) {
  switch (action.type) {
    case 'LOGIN_REQUEST':
      return { ...state, loading: true };
    case 'LOGIN_SUCCESS':
      return { ...state, loading: false, user: action.user };
    case 'LOGIN_FAILURE':
      return { ...state, loading: false, error: action.error };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Handling complex state objects or arrays:

If your state is an object or array with several sub-properties that require specific updates, useReducer can be used to structure and manage the state more effectively.
Example: Managing a form with different input fields, each with its own error message, validity, and other related data.

  • Decoupling UI logic from business logic:

useReducer is useful when you want to separate how the UI updates from the business logic that defines how the state should change.
Example: For complex workflows, like a multi-step form, that require a defined flow for state transitions.

Limitations of useReducer:

  • More boilerplate:

useReducer introduces more boilerplate code compared to useState. You'll need to define a reducer function and action types, which might feel overkill for simple state management.
Example: Writing a reducer function with a switch statement can be more verbose than simply using useState.

  • Learning curve:

For beginners, useReducer might seem like overkill because it requires understanding of reducers, actions, and the concept of immutability. It’s a bit harder to get started with compared to useState.

  • Not necessary for simple state:

If your component’s state is simple, like a single boolean or number, using useReducer adds unnecessary complexity. It’s better to reserve useReducer for scenarios where you have complex state logic.

  • Dispatching actions and managing flow:

If you don’t need to centralize your state logic, managing each action type and dispatching it properly can lead to unnecessary complexity, especially if you're only working with a small amount of state.

Comparison:

compare image

Which One to Use?

1. Use useState if:
  • The state is simple (e.g., a number, string, or boolean).
  • You don't have complex state transitions.
  • You have one or a few state variables that are not interdependent.
2. Use useReducer if:
  • The state logic is complex or involves multiple actions that update different parts of state.
  • You have a state that changes based on multiple conditions.
  • You want a more organized and scalable way of managing state in large components.

Best Practice:

  • If you’re just building a small form or handling simple interactions like toggling visibility, useState is often the best choice.
  • For more advanced or large components with multiple, interdependent pieces of state or complex updates (e.g., multi-step forms, managing arrays/objects, etc.), useReducer can lead to more maintainable code.

Top comments (0)