React State Hooks

State is one of the most important concepts in React.

It allows components to remember information between renders and update the user interface when that information changes.

You have already learned useState, which is the simplest and most commonly used State Hook.

React also provides useReducer, which is useful when state logic becomes more complex.

Both Hooks solve the same broad problem:

How should a component remember and update information?

The difference is mainly in how the state transitions are organized.

A simple component might use:

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

A component with many related state transitions might use:

const [state, dispatch] = useReducer(
  reducer,
  initialState
);

In this chapter, you will learn:

  • useState
  • useReducer
  • Reducers
  • Actions
  • Dispatch
  • Complex state management
  • When to choose useState
  • When useReducer makes more sense

useState

useState is a React Hook used to add state to a function component.

You import it from React:

import { useState } from "react";

Then call it:

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

The Hook returns an array containing two values:

Current state
State setter

In this example:

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

count contains the current value.

setCount is the function used to request a state update.

Basic Example

import { useState } from "react";

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

  return (
    <div>
      <p>Count: {count}</p>

      <button
        onClick={() =>
          setCount((value) => value + 1)
        }
      >
        Increase
      </button>
    </div>
  );
}

When the button is clicked, React schedules a state update and renders the component again with the new state.

State Can Store Different Types

useState can store:

const [name, setName] = useState("");

A string.

const [age, setAge] = useState(25);

A number.

const [isOpen, setIsOpen] = useState(false);

A boolean.

It can also store objects:

const [user, setUser] = useState({
  name: "Alex",
  age: 25
});

And arrays:

const [items, setItems] = useState([]);

Updating State

For a simple value:

setCount(10);

For an update based on the previous value:

setCount((currentCount) =>
  currentCount + 1
);

The functional form is especially useful when the next value depends on the previous value.

Updating Objects

Do not directly mutate the state object.

Avoid:

user.name = "John";

Instead:

setUser((currentUser) => ({
  ...currentUser,
  name: "John"
}));

The spread operator creates a new object while preserving the properties you did not change.

Updating Arrays

Avoid:

items.push(newItem);

Instead:

setItems((currentItems) => [
  ...currentItems,
  newItem
]);

For removing an item:

setItems((currentItems) =>
  currentItems.filter(
    (item) => item.id !== itemId
  )
);

useState is usually the right choice when state updates are straightforward.

When Should You Use useState?

useState works well when:

  • A component has a few state values
  • Updates are simple
  • State transitions are easy to understand
  • You do not need a large collection of related actions
  • The update logic can remain close to the component

For example:

const [isMenuOpen, setIsMenuOpen] =
  useState(false);

There is little benefit in using a reducer for such a simple piece of state.

useReducer

useReducer is another State Hook.

It is useful when state updates involve more complex logic or multiple related transitions.

You import it from React:

import { useReducer } from "react";

The basic syntax is:

const [state, dispatch] = useReducer(
  reducer,
  initialState
);

It returns:

  • state: the current state
  • dispatch: a function used to request a state transition

The reducer determines how the state should change.

Basic Example

import { useReducer } from "react";

function reducer(state, action) {
  if (action.type === "increment") {
    return {
      count: state.count + 1
    };
  }

  return state;
}

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

  return (
    <div>
      <p>{state.count}</p>

      <button
        onClick={() =>
          dispatch({ type: "increment" })
        }
      >
        Increase
      </button>
    </div>
  );
}

The flow is:

User interaction
      ↓
dispatch(action)
      ↓
reducer(state, action)
      ↓
new state
      ↓
React renders UI

This separates the description of an event from the logic that determines how state changes.

Why Use useReducer?

Imagine an application with state such as:

const [user, setUser] = useState({
  name: "",
  email: "",
  age: 0,
  isLoggedIn: false,
  isLoading: false,
  error: null
});

There may be many different operations:

  • Update name
  • Update email
  • Update age
  • Start loading
  • Finish loading
  • Log in
  • Log out
  • Set an error
  • Clear an error

With enough transitions, a large collection of setUser calls can become difficult to follow.

A reducer can centralize these transitions.

const [state, dispatch] = useReducer(
  reducer,
  initialState
);

Then actions describe what happened:

dispatch({
  type: "update_email",
  value: "alex@example.com"
});

The reducer determines how that event changes the state.

Reducers

A reducer is a function that determines the next state based on the current state and an action.

The standard shape is:

function reducer(state, action) {
  // return next state
}

A reducer receives:

Current state
+
Action

and returns:

Next state

Conceptually:

(state, action)
      ↓
   reducer
      ↓
  new state

Simple Reducer

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return {
        ...state,
        count: state.count + 1
      };

    case "decrement":
      return {
        ...state,
        count: state.count - 1
      };

    default:
      return state;
  }
}

The reducer does not directly update the existing state object.

Instead, it returns a new state.

Reducers Should Be Pure

A reducer should be a pure function.

That means it should calculate the next state from its inputs without causing unrelated side effects.

For example:

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return {
        ...state,
        count: state.count + 1
      };

    default:
      return state;
  }
}

This is predictable.

Avoid putting things such as network requests, timers, or DOM manipulation directly inside a reducer.

Instead, handle those operations outside the reducer and dispatch actions based on what happens.

Do Not Mutate State in a Reducer

Avoid:

function reducer(state, action) {
  state.count++;

  return state;
}

Instead:

function reducer(state, action) {
  return {
    ...state,
    count: state.count + 1
  };
}

The reducer should return the next state rather than mutating the existing state.

Image Placement: Reducer Flow

Place this image immediately after the explanation of reducers.

Create a modern 16:9 educational infographic titled “How useReducer Works in React”. Show Current State flowing into a Reducer, alongside an Action, producing New State, which then updates the UI. Include the flow dispatch(action) → reducer(state, action) → new state → render. Use a polished Web4Learners developer aesthetic with rounded cards, subtle gradients, clean typography, minimal text, and a modern React-inspired interface.

Actions

An action is an object that describes what happened.

A typical action contains a type:

{
  type: "increment"
}

For actions that need additional information, you can include a payload or other fields:

{
  type: "set_name",
  value: "Alex"
}

Or:

{
  type: "add_item",
  item: {
    id: 1,
    name: "Laptop"
  }
}

The action does not normally perform the state update itself.

It describes the event.

The reducer decides how the state should change.

Action Example

Suppose the state is:

{
  count: 0
}

You can dispatch:

dispatch({
  type: "increment"
});

The reducer receives:

function reducer(state, action) {

and checks:

action.type

Then:

case "increment":

returns the new state.

Actions Describe Events

A useful way to think about actions is:

"increment"
"item_added"
"item_removed"
"logged_in"
"logged_out"
"form_submitted"

They describe something that happened.

For example:

dispatch({
  type: "item_added",
  item: newItem
});

The action communicates:

An item was added.

The reducer determines what that means for the state.

Payload

A payload contains additional information needed by the reducer.

For example:

dispatch({
  type: "set_quantity",
  productId: 10,
  quantity: 3
});

The reducer can use those values:

function reducer(state, action) {
  switch (action.type) {
    case "set_quantity":
      return {
        ...state,
        quantity: action.quantity
      };

    default:
      return state;
  }
}

The exact action structure is a design choice.

The important principle is to make actions clear and predictable.

Dispatch

dispatch is the function returned by useReducer.

For example:

const [state, dispatch] = useReducer(
  reducer,
  initialState
);

You use dispatch to send an action to the reducer.

dispatch({
  type: "increment"
});

React then uses that action with the reducer to determine the next state.

Dispatching Different Actions

Suppose:

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return {
        ...state,
        count: state.count + 1
      };

    case "decrement":
      return {
        ...state,
        count: state.count - 1
      };

    case "reset":
      return {
        ...state,
        count: 0
      };

    default:
      return state;
  }
}

You can dispatch:

dispatch({
  type: "increment"
});

or:

dispatch({
  type: "decrement"
});

or:

dispatch({
  type: "reset"
});

The UI does not need to know how each state transition is implemented.

It simply describes what happened.

Complete useReducer Counter

Here is a complete example:

import { useReducer } from "react";

const initialState = {
  count: 0
};

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return {
        ...state,
        count: state.count + 1
      };

    case "decrement":
      return {
        ...state,
        count: state.count - 1
      };

    case "reset":
      return initialState;

    default:
      return state;
  }
}

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

  return (
    <div>
      <p>Count: {state.count}</p>

      <button
        onClick={() =>
          dispatch({ type: "increment" })
        }
      >
        +
      </button>

      <button
        onClick={() =>
          dispatch({ type: "decrement" })
        }
      >
        -
      </button>

      <button
        onClick={() =>
          dispatch({ type: "reset" })
        }
      >
        Reset
      </button>
    </div>
  );
}

The component has three possible actions:

increment
decrement
reset

The reducer contains the state-transition logic.

Passing Data with Actions

Actions become more useful when they carry information.

Imagine a shopping cart.

dispatch({
  type: "add_item",
  item: {
    id: 1,
    name: "Keyboard",
    price: 50
  }
});

The reducer can handle it:

function reducer(state, action) {
  switch (action.type) {
    case "add_item":
      return {
        ...state,
        items: [
          ...state.items,
          action.item
        ]
      };

    default:
      return state;
  }
}

The component describes the event.

The reducer handles the state transition.

Complex State Management

useReducer becomes particularly useful when state contains several related values and many different ways to update them.

For example:

const initialState = {
  items: [],
  total: 0,
  status: "idle",
  error: null
};

Possible events include:

add item
remove item
update quantity
start checkout
checkout success
checkout failure
clear cart

Managing all of these transitions with independent setters can become difficult.

A reducer provides a central place for the state transition logic.

Shopping Cart Example

Let’s build a simplified shopping cart.

const initialState = {
  items: []
};

function reducer(state, action) {
  switch (action.type) {
    case "add_item":
      return {
        ...state,
        items: [
          ...state.items,
          action.item
        ]
      };

    case "remove_item":
      return {
        ...state,
        items: state.items.filter(
          (item) =>
            item.id !== action.itemId
        )
      };

    case "clear_cart":
      return {
        ...state,
        items: []
      };

    default:
      return state;
  }
}

The component:

function ShoppingCart() {
  const [state, dispatch] = useReducer(
    reducer,
    initialState
  );

  function addItem(item) {
    dispatch({
      type: "add_item",
      item
    });
  }

  function removeItem(itemId) {
    dispatch({
      type: "remove_item",
      itemId
    });
  }

  function clearCart() {
    dispatch({
      type: "clear_cart"
    });
  }

  return (
    <div>
      <p>
        Items: {state.items.length}
      </p>

      <button
        onClick={() =>
          addItem({
            id: 1,
            name: "Keyboard"
          })
        }
      >
        Add Keyboard
      </button>

      <button onClick={clearCart}>
        Clear Cart
      </button>
    </div>
  );
}

As the application grows, more actions can be added without spreading state-transition logic throughout the component.

useState vs useReducer

Both Hooks manage state, but they have different strengths.

useState

Use useState when state transitions are simple.

const [isOpen, setIsOpen] =
  useState(false);

This is easy to understand.

useReducer

Use useReducer when many related transitions exist.

const [state, dispatch] =
  useReducer(reducer, initialState);

This makes state transitions explicit.

Comparison

useStateuseReducer
Simple stateMore complex state
Direct setterDispatch actions
Update logic often near event handlerUpdate logic centralized in reducer
Easy for small componentsUseful for many related transitions
Good for independent valuesGood for state with related transitions

Neither Hook is automatically better.

Choose the simplest approach that makes the state logic clear.

Converting useState to useReducer

Suppose you start with:

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

You might update it with:

setCount((value) => value + 1);

With a reducer:

const [count, dispatch] =
  useReducer(reducer, 0);

Then:

dispatch({
  type: "increment"
});

and:

function reducer(count, action) {
  switch (action.type) {
    case "increment":
      return count + 1;

    default:
      return count;
  }
}

For a single counter, useReducer may be more code than necessary.

But if the state later becomes more complex, the reducer pattern can become more useful.

Complex Forms with useReducer

Forms are another common use case.

Suppose a form contains:

Name
Email
Password
Role
Newsletter

You could use several useState calls.

Or you could use:

const initialState = {
  name: "",
  email: "",
  password: "",
  role: "",
  newsletter: false,
  errors: {}
};

The reducer can manage different events.

function reducer(state, action) {
  switch (action.type) {
    case "field_changed":
      return {
        ...state,
        [action.field]: action.value
      };

    case "set_errors":
      return {
        ...state,
        errors: action.errors
      };

    case "reset":
      return initialState;

    default:
      return state;
  }
}

Then an input can dispatch:

dispatch({
  type: "field_changed",
  field: "email",
  value: event.target.value
});

This approach can be useful when a form has many related state transitions.

Reducers with Nested Objects

Reducers can also manage nested state.

For example:

const initialState = {
  user: {
    profile: {
      name: "",
      city: ""
    }
  }
};

To update the city:

function reducer(state, action) {
  switch (action.type) {
    case "city_changed":
      return {
        ...state,
        user: {
          ...state.user,
          profile: {
            ...state.user.profile,
            city: action.city
          }
        }
      };

    default:
      return state;
  }
}

Each level that changes needs to be represented by a new object.

For deeply nested state, you may want to reconsider the state structure if updates become unnecessarily difficult.

Reducer Composition

As applications grow, one reducer can become large.

You can organize related state logic into smaller reducers or other abstractions when appropriate.

For example:

Application
 |
 +-- user state
 |
 +-- cart state
 |
 +-- notification state

Each area can have its own state logic.

However, do not split reducers simply because the file is large. Split them when doing so makes the application’s state model easier to understand.

Reducer Actions Should Be Meaningful

Compare:

dispatch({
  type: "setState",
  value: {
    count: 5
  }
});

with:

dispatch({
  type: "increment"
});

The second action communicates the event more clearly.

For more specific operations:

dispatch({
  type: "item_removed",
  itemId: 42
});

This describes what happened.

Meaningful action types make reducer logic easier to understand and debug.

Action Names

There is no single required naming style.

You may see:

increment
decrement
reset
add_item
remove_item

or:

INCREMENT
DECREMENT
RESET
ADD_ITEM
REMOVE_ITEM

Modern JavaScript applications commonly use lowercase strings, but the important thing is consistency within the project.

Default Case in a Reducer

A reducer should handle unknown actions safely.

For example:

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return {
        ...state,
        count: state.count + 1
      };

    default:
      return state;
  }
}

Returning the existing state for an unknown action is a common pattern.

Avoid silently returning an unrelated state structure.

Reducers and Side Effects

Reducers should calculate state.

They should not normally perform side effects such as:

API requests
Timers
DOM manipulation
Logging as application behavior
Random external operations

For example, avoid:

function reducer(state, action) {
  fetch("/api/products");

  return state;
}

Instead, perform the external operation elsewhere and dispatch an action based on the result.

Conceptually:

Component / Action
      ↓
External operation
      ↓
dispatch()
      ↓
Reducer
      ↓
New state

This keeps the reducer predictable.

dispatch Does Not Directly Change State

When you write:

dispatch({
  type: "increment"
});

you are not directly modifying the state variable.

You are requesting a state transition.

React processes the update using the reducer and then renders the component with the resulting state.

This is similar to the way a state setter schedules an update with useState.

The current render’s state remains a snapshot.

Image Placement: State Management Comparison

Place this image before the useState vs useReducer comparison.

Create a modern 16:9 educational infographic comparing useState and useReducer. On the left show useState with a simple toggle or counter and direct state setter. On the right show useReducer with dispatch(action) → reducer → new state. Include a subtle visual cue that useState is suited to simpler transitions while useReducer is useful for more complex related state logic. Use a polished Web4Learners developer aesthetic with rounded cards, subtle gradients, clean typography, minimal text, and modern UI.

A Complete Complex State Example

Let’s build a small task manager.

The application needs to:

  • Add tasks
  • Remove tasks
  • Toggle completion
  • Clear completed tasks

This is a good situation for useReducer.

import { useReducer } from "react";

const initialState = {
  tasks: []
};

function reducer(state, action) {
  switch (action.type) {
    case "task_added":
      return {
        ...state,
        tasks: [
          ...state.tasks,
          {
            id: crypto.randomUUID(),
            text: action.text,
            completed: false
          }
        ]
      };

    case "task_toggled":
      return {
        ...state,
        tasks: state.tasks.map((task) =>
          task.id === action.id
            ? {
                ...task,
                completed: !task.completed
              }
            : task
        )
      };

    case "task_removed":
      return {
        ...state,
        tasks: state.tasks.filter(
          (task) => task.id !== action.id
        )
      };

    case "completed_cleared":
      return {
        ...state,
        tasks: state.tasks.filter(
          (task) => !task.completed
        )
      };

    default:
      return state;
  }
}

function TaskManager() {
  const [state, dispatch] =
    useReducer(
      reducer,
      initialState
    );

  function addTask(text) {
    dispatch({
      type: "task_added",
      text
    });
  }

  return (
    <div>
      <button
        onClick={() =>
          addTask("Learn React")
        }
      >
        Add Task
      </button>

      {state.tasks.map((task) => (
        <div key={task.id}>
          <span>
            {task.text}
          </span>

          <button
            onClick={() =>
              dispatch({
                type: "task_toggled",
                id: task.id
              })
            }
          >
            Toggle
          </button>

          <button
            onClick={() =>
              dispatch({
                type: "task_removed",
                id: task.id
              })
            }
          >
            Remove
          </button>
        </div>
      ))}

      <button
        onClick={() =>
          dispatch({
            type: "completed_cleared"
          })
        }
      >
        Clear Completed
      </button>
    </div>
  );
}

There are several important ideas here.

The State Is Centralized

The task collection lives in:

state.tasks

Events Are Described with Actions

Adding a task:

{
  type: "task_added",
  text
}

Toggling a task:

{
  type: "task_toggled",
  id
}

Removing a task:

{
  type: "task_removed",
  id
}

The Reducer Owns the Transition Logic

The component does not need to know how a task is added, removed, or toggled.

It only dispatches an action.

The reducer decides what the next state should be.

useReducer with Context

useReducer can also be combined with Context when multiple components need access to the same complex state.

A common architecture is:

Context Provider
      |
      +-- State
      |
      +-- Dispatch
      |
      +-- Child components

For example:

const [state, dispatch] =
  useReducer(reducer, initialState);

Then the provider can make both available through Context.

Child components can read:

state

and send:

dispatch(action)

This pattern is useful for some application-level state, although it should not be used automatically for every piece of state.

You will explore Context and shared state patterns in more detail later.

When useReducer May Be a Better Fit

Consider useReducer when you notice patterns such as:

Many State Setters

If a component contains many related setters:

setUser(...)
setItems(...)
setError(...)
setStatus(...)
setSelected(...)

and these values change together in response to well-defined events, a reducer may make the transitions easier to understand.

Complex Conditional Updates

If every update contains complicated logic:

if (...)
  if (...)
    ...

a reducer can centralize those transitions.

Related State Values

If several state values describe one process, a reducer can represent that process as a single state object.

Many User Actions

Applications such as:

  • Shopping carts
  • Task managers
  • Multi-step forms
  • Editors
  • Games
  • Complex filters

can sometimes benefit from reducer-based state management.

When useState Is Better

Do not use useReducer simply because it is more advanced.

For this:

const [isOpen, setIsOpen] =
  useState(false);

a reducer would add unnecessary code.

Similarly:

const [name, setName] =
  useState("");

is straightforward.

Use useState when it keeps the state logic simpler.

Common Mistakes with useReducer

Mutating State

Avoid:

state.items.push(action.item);
return state;

Instead:

return {
  ...state,
  items: [
    ...state.items,
    action.item
  ]
};

Forgetting to Return State

Every handled reducer case needs to return the appropriate next state.

Avoid:

case "increment":
  state.count + 1;

Instead:

case "increment":
  return {
    ...state,
    count: state.count + 1
  };

Mutating Nested Objects

Avoid:

state.user.name = "Alex";
return state;

Instead:

return {
  ...state,
  user: {
    ...state.user,
    name: "Alex"
  }
};

Putting API Calls Inside Reducers

Reducers should calculate state transitions.

Perform external operations outside the reducer.

Creating Unclear Actions

Avoid vague actions such as:

{
  type: "update"
}

when the application has many different types of updates.

Prefer meaningful actions:

{
  type: "email_changed",
  value: email
}

Using useReducer for Everything

A reducer is not automatically better than useState.

For simple state, useState is usually easier to read.

Best Practices for State Hooks

Start with useState

If your state logic is simple, start with useState.

You can move to useReducer if the state transitions become difficult to manage.

Keep Reducers Pure

A reducer should calculate the next state from:

state
+
action

Do Not Mutate State

Always return new objects or arrays when updating them.

Use Meaningful Action Types

Action names should communicate what happened.

Keep Related State Together

If several values belong to the same state machine or workflow, a reducer can make their relationships clearer.

Avoid Unnecessary State

If something can be calculated from existing state, consider deriving it rather than storing another copy.

Keep Reducer Logic Focused

The reducer should handle state transitions, not unrelated application responsibilities.

Use Stable IDs for Collections

When reducer-managed state contains lists, use stable identifiers for items and React keys.

State Hook Decision Guide

When you need state, ask these questions:

Is the state simple?

For example:

const [isOpen, setIsOpen] =
  useState(false);

Use useState.

Are there several related transitions?

For example:

add
remove
update
reset
submit
error
success

Consider useReducer.

Does the next value depend on the previous value?

With useState, use a functional update:

setCount((value) => value + 1);

With useReducer, the reducer receives the current state:

function reducer(state, action) {
  return {
    ...state,
    count: state.count + 1
  };
}

Do several components need the state?

Consider lifting the state to a common parent or using another appropriate sharing mechanism.

State Hooks Cheat Sheet

ConceptPurpose
useStateManage straightforward component state
useReducerManage state with more complex transitions
ReducerCalculates the next state
ActionDescribes what happened
dispatchSends an action to the reducer
Initial stateStarting state for the component
Functional updateCalculates new state from previous state
Immutable updateCreates new objects or arrays instead of mutating existing state
PayloadAdditional information carried by an action
Pure reducerReducer without unrelated side effects
Complex stateState with multiple related values and transitions

useState vs useReducer Quick Example

useState

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

setCount((value) => value + 1);

Best when the update is simple.

useReducer

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

dispatch({
  type: "increment"
});

Best when state transitions benefit from explicit actions and centralized logic.

Neither pattern is inherently superior.

The right choice depends on the complexity of the state and how the component changes it.

Practice Exercise

Build a Web4Learners Course Manager using useReducer.

The state should contain:

{
  courses: []
}

Each course should have:

{
  id,
  title,
  completed
}

Create these actions:

course_added
course_completed
course_removed
courses_cleared

Your reducer should handle all four actions.

Add a Course

Dispatch:

dispatch({
  type: "course_added",
  title: "React Hooks"
});

Complete a Course

Dispatch:

dispatch({
  type: "course_completed",
  id: courseId
});

Remove a Course

Dispatch:

dispatch({
  type: "course_removed",
  id: courseId
});

Clear Courses

Dispatch:

dispatch({
  type: "courses_cleared"
});

Then display:

  • Course title
  • Completion status
  • Complete button
  • Remove button
  • Add Course button
  • Clear Courses button

As an additional challenge, derive:

const completedCount = ...

from the courses instead of storing the count as separate state.

This will reinforce the relationship between reducers, actions, dispatch, immutable updates, and derived data.

Key Takeaways

  • useState and useReducer are React State Hooks.
  • useState is generally suitable for simple state and straightforward updates.
  • useReducer is useful when state has multiple related transitions or complex update logic.
  • A reducer receives the current state and an action and returns the next state.
  • An action describes what happened.
  • dispatch sends an action to the reducer.
  • Actions can contain additional information such as IDs, values, or objects.
  • Reducers should be pure and should not perform unrelated side effects.
  • Reducers should not directly mutate existing state.
  • Objects and arrays should be updated immutably.
  • Meaningful action names make reducer logic easier to understand.
  • useReducer can make complex state transitions more centralized and predictable.
  • useReducer is not automatically better than useState.
  • Start with the simpler state management approach and introduce a reducer when it makes the logic clearer.
  • Complex forms, shopping carts, task managers, editors, and other state-heavy interfaces can sometimes benefit from reducers.
  • Derived values should usually be calculated from existing state rather than stored as duplicate state.
  • useReducer can be combined with Context when complex state needs to be shared across many components.
  • Good state management is less about choosing the most advanced Hook and more about choosing a structure that makes the application’s behavior easy to understand.

Leave a Comment