We use cookies to enhance your experience on the site
CodeWorlds

useReducer - advanced state management

In a spaceship's command center, a simple panel with one button is not enough. When onboard systems become increasingly complex - shields, engines, navigation systems, alarms - we need a more organized way to manage state. The

useReducer
hook is like the central onboard computer that receives orders (actions) and updates the state of all systems based on strictly defined rules.

What is useReducer?

useReducer
is an alternative to
useState
, designed to handle more complex state logic. Instead of directly setting a new value, you send an action (command), and a special function called a reducer decides how the state should change.

Basic syntax

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

Where:

  • state
    - the current state (ship system data)
  • dispatch
    - the function for sending actions (issuing commands)
  • reducer
    - a function that returns a new state based on the current state and action
  • initialState
    - the initial state value

Space analogy: Think of

dispatch
as the control panel where you press buttons (actions). The onboard computer (reducer) receives the command and calculates the new state based on the ship's current state. You never modify systems manually - always through the computer!

The reducer function

A reducer is a pure function that takes two arguments: the current state and an action. Based on these, it returns a new state object. We most commonly use a

switch
statement to handle different action types:

1function spaceshipReducer(state, action) {
2  switch (action.type) {
3    case 'ACTIVATE_SHIELDS':
4      return { ...state, shields: 100, status: 'defended' };
5
6    case 'FIRE_ENGINES':
7      return { ...state, speed: state.speed + action.payload, fuel: state.fuel - 10 };
8
9    case 'EMERGENCY_STOP':
10      return { ...state, speed: 0, status: 'stopped' };
11
12    default:
13      return state;
14  }
15}

Key reducer rules:

  • Always return a new state object (don't mutate the previous one)
  • Handle
    default
    - return the unchanged state for unknown actions
  • The reducer should be a pure function - no side effects, no randomness

Dispatch and action objects

An action is a plain JavaScript object with a

type
field describing the kind of operation. You can optionally add a
payload
field with additional data:

1// Simple action - type only
2dispatch({ type: 'ACTIVATE_SHIELDS' });
3
4// Action with data (payload)
5dispatch({ type: 'FIRE_ENGINES', payload: 50 });
6
7// Action with complex payload
8dispatch({
9  type: 'ADD_CREW_MEMBER',
10  payload: { name: 'Captain Nova', role: 'pilot' }
11});

Practical example - ship control panel

Let's see how

useReducer
performs in managing complex control panel state:

1import { useReducer } from 'react';
2
3const initialShipState = {
4  fuel: 100,
5  shields: 0,
6  speed: 0,
7  status: 'docked',
8  log: []
9};
10
11function shipReducer(state, action) {
12  switch (action.type) {
13    case 'LAUNCH':
14      if (state.fuel < 20) return state;
15      return {
16        ...state,
17        status: 'flying',
18        fuel: state.fuel - 20,
19        speed: 50,
20        log: [...state.log, 'Ship launched']
21      };
22
23    case 'BOOST':
24      if (state.status !== 'flying' || state.fuel < 10) return state;
25      return {
26        ...state,
27        speed: state.speed + action.payload,
28        fuel: state.fuel - 10,
29        log: [...state.log, `Accelerated by ${action.payload} km/s`]
30      };
31
32    case 'TOGGLE_SHIELDS':
33      return {
34        ...state,
35        shields: state.shields > 0 ? 0 : 100,
36        log: [...state.log, state.shields > 0 ? 'Shields deactivated' : 'Shields activated']
37      };
38
39    case 'DOCK':
40      return {
41        ...state,
42        status: 'docked',
43        speed: 0,
44        log: [...state.log, 'Ship docked']
45      };
46
47    case 'REFUEL':
48      return {
49        ...state,
50        fuel: 100,
51        log: [...state.log, 'Fuel replenished']
52      };
53
54    default:
55      return state;
56  }
57}
58
59function ShipControlPanel() {
60  const [ship, dispatch] = useReducer(shipReducer, initialShipState);
61
62  return (
63    <div className="control-panel">
64      <h2>Control panel</h2>
65      <p>Status: {ship.status}</p>
66      <p>Fuel: {ship.fuel}%</p>
67      <p>Shields: {ship.shields}%</p>
68      <p>Speed: {ship.speed} km/s</p>
69
70      <button onClick={() => dispatch({ type: 'LAUNCH' })}
71        disabled={ship.status === 'flying'}>
72        Launch
73      </button>
74      <button onClick={() => dispatch({ type: 'BOOST', payload: 25 })}
75        disabled={ship.status !== 'flying'}>
76        Boost (+25)
77      </button>
78      <button onClick={() => dispatch({ type: 'TOGGLE_SHIELDS' })}>
79        {ship.shields > 0 ? 'Deactivate shields' : 'Activate shields'}
80      </button>
81      <button onClick={() => dispatch({ type: 'DOCK' })}
82        disabled={ship.status === 'docked'}>
83        Dock
84      </button>
85      <button onClick={() => dispatch({ type: 'REFUEL' })}
86        disabled={ship.status === 'flying'}>
87        Refuel
88      </button>
89
90      <div className="ship-log">
91        <h3>Ship log:</h3>
92        <ul>
93          {ship.log.map((entry, i) => (
94            <li key={i}>{entry}</li>
95          ))}
96        </ul>
97      </div>
98    </div>
99  );
100}

Notice how the reducer centralizes all the state update logic. The component doesn't need to know exactly how the state changes - it just needs to send the appropriate action. This is a huge advantage for complex interfaces.

When to use useReducer instead of useState?

Both hooks serve to manage state, but they have different use cases:

Choose

useState
when:

  • The state is simple (number, string, boolean)
  • The update logic is simple (direct assignment)
  • You have several independent values

Choose

useReducer
when:

  • The state is a complex object with many fields
  • Updating one field requires changing other fields
  • You have many related actions (e.g., start, stop, reset, boost)
  • The update logic contains conditions (e.g., don't launch without fuel)
  • You want to easily test state logic separately from the component
1// useState - sufficient for simple cases
2const [isOpen, setIsOpen] = useState(false);
3const [count, setCount] = useState(0);
4
5// useReducer - better for complex state
6const [shipState, dispatch] = useReducer(shipReducer, {
7  fuel: 100,
8  shields: 0,
9  speed: 0,
10  status: 'docked',
11  crew: [],
12  alerts: [],
13  systems: { engine: 'online', navigation: 'online', weapons: 'offline' }
14});

The switch-case pattern in reducers

The

switch
statement is the standard pattern in reducers. Here are some best practices:

1// Define constants for action types - avoid typos
2const ACTIONS = {
3  LAUNCH: 'LAUNCH',
4  DOCK: 'DOCK',
5  BOOST: 'BOOST',
6  TAKE_DAMAGE: 'TAKE_DAMAGE',
7  REPAIR: 'REPAIR'
8};
9
10function shipReducer(state, action) {
11  switch (action.type) {
12    case ACTIONS.LAUNCH:
13      return { ...state, status: 'flying', speed: 50 };
14
15    case ACTIONS.DOCK:
16      return { ...state, status: 'docked', speed: 0 };
17
18    case ACTIONS.BOOST:
19      return {
20        ...state,
21        speed: Math.min(state.speed + action.payload, 300)
22      };
23
24    case ACTIONS.TAKE_DAMAGE: {
25      const newShields = Math.max(0, state.shields - action.payload);
26      return {
27        ...state,
28        shields: newShields,
29        status: newShields === 0 ? 'critical' : state.status
30      };
31    }
32
33    case ACTIONS.REPAIR:
34      return { ...state, shields: 100, status: 'docked' };
35
36    default:
37      throw new Error(`Unknown action: ${action.type}`);
38  }
39}
40
41// Usage with constants
42dispatch({ type: ACTIONS.BOOST, payload: 50 });
43dispatch({ type: ACTIONS.TAKE_DAMAGE, payload: 30 });

Notice that in

TAKE_DAMAGE
we used a block
{}
inside the
case
- this allows declaring local variables (
newShields
) without conflicts with other cases. Additionally, instead of
default: return state
, we throw an error - this helps catch typos in action names during development.

Action creators

For convenience and readability, you can create functions that generate action objects:

1// Action creators
2const launch = () => ({ type: 'LAUNCH' });
3const boost = (amount) => ({ type: 'BOOST', payload: amount });
4const takeDamage = (amount) => ({ type: 'TAKE_DAMAGE', payload: amount });
5const addCrewMember = (name, role) => ({
6  type: 'ADD_CREW_MEMBER',
7  payload: { name, role }
8});
9
10// Usage - more readable than manually creating objects
11dispatch(launch());
12dispatch(boost(50));
13dispatch(takeDamage(25));
14dispatch(addCrewMember('Nova', 'pilot'));

Action creators reduce the risk of typos and centralize action creation logic in one place.

Summary

useReducer
is a powerful tool for managing complex state in React:

  1. Syntax:
    const [state, dispatch] = useReducer(reducer, initialState)
  2. Reducer: a pure function
    (state, action) => newState
    with the switch-case pattern
  3. Dispatch: sending actions
    dispatch({ type: 'ACTION', payload: data })
  4. When to use: complex state, related updates, business logic in state
  5. Best practices: constants for action types, action creators,
    {}
    blocks in case, throwing errors in default

Just as the central onboard computer of a spaceship coordinates the work of all systems based on commands from the captain's bridge,

useReducer
centralizes state logic, making it predictable and easy to manage.

Go to CodeWorlds