JavaScript and React course Β· Module 4: Context API and useReducer

useReducer - advanced state management

7 min read
In this lesson8

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.

Code for this lesson: App.jsx
1import React, { useState, useMemo, useCallback, memo } from 'react';
2
3// Component without optimization (for comparison)
4function UnoptimizedItem({ name, onSelect }) {
5  console.log('Rendering UnoptimizedItem:', name);
6  return (
7    <div style={{
8      padding: '10px',
9      backgroundColor: '#ffebee',
10      marginBottom: '5px',
11      borderRadius: '4px'
12    }}>
13      {name}
14      <button onClick={() => onSelect(name)} style={{ marginLeft: '10px' }}>Select</button>
15    </div>
16  );
17}
18
19// Component optimized with React.memo
20const OptimizedItem = memo(function OptimizedItem({ name, onSelect }) {
21  console.log('Rendering OptimizedItem:', name);
22  return (
23    <div style={{
24      padding: '10px',
25      backgroundColor: '#e8f5e9',
26      marginBottom: '5px',
27      borderRadius: '4px'
28    }}>
29      {name}
30      <button onClick={() => onSelect(name)} style={{ marginLeft: '10px' }}>Select</button>
31    </div>
32  );
33});
34
35// Heavy computations (simulation)
36function expensiveCalculation(numbers) {
37  console.log('Running expensive calculation...');
38  let result = 0;
39  for (let i = 0; i < 100000000; i++) {
40    result += numbers.reduce((a, b) => a + b, 0);
41  }
42  return numbers.reduce((a, b) => a + b, 0);
43}
44
45function PerformanceDemo() {
46  const [count, setCount] = useState(0);
47  const [numbers] = useState([1, 2, 3, 4, 5]);
48  const [selectedItem, setSelectedItem] = useState('');
49  const [darkMode, setDarkMode] = useState(false);
50
51  const items = ['Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune'];
52
53  // Without useMemo - computation on every render
54  // const sum = expensiveCalculation(numbers);
55
56  // With useMemo - computation only when 'numbers' changes
57  const sum = useMemo(() => {
58    console.log('Computing sum with useMemo...');
59    return numbers.reduce((a, b) => a + b, 0);
60  }, [numbers]);
61
62  // Without useCallback - new function on every render
63  // const handleSelect = (name) => setSelectedItem(name);
64
65  // With useCallback - stable reference
66  const handleSelect = useCallback((name) => {
67    setSelectedItem(name);
68  }, []);
69
70  // Styles optimized with useMemo
71  const containerStyle = useMemo(() => ({
72    fontFamily: 'Arial, sans-serif',
73    padding: '20px',
74    maxWidth: '900px',
75    margin: '0 auto',
76    backgroundColor: darkMode ? '#1a1a2e' : '#f5f5f5',
77    color: darkMode ? 'white' : '#333',
78    minHeight: '100vh',
79    transition: 'all 0.3s ease'
80  }), [darkMode]);
81
82  return (
83    <div style={containerStyle}>
84      <h1 style={{ textAlign: 'center', color: '#667eea' }}>Performance Optimization</h1>
85
86      <div style={{
87        display: 'flex',
88        justifyContent: 'space-between',
89        alignItems: 'center',
90        marginBottom: '30px',
91        padding: '15px',
92        backgroundColor: darkMode ? '#252545' : '#fff',
93        borderRadius: '12px'
94      }}>
95        <div>
96          <button
97            onClick={() => setCount(c => c + 1)}
98            style={{
99              padding: '10px 20px',
100              backgroundColor: '#667eea',
101              color: 'white',
102              border: 'none',
103              borderRadius: '8px',
104              cursor: 'pointer',
105              marginRight: '10px'
106            }}
107          >
108            Count: {count}
109          </button>
110          <button
111            onClick={() => setDarkMode(!darkMode)}
112            style={{
113              padding: '10px 20px',
114              backgroundColor: darkMode ? '#fff' : '#333',
115              color: darkMode ? '#333' : 'white',
116              border: 'none',
117              borderRadius: '8px',
118              cursor: 'pointer'
119            }}
120          >
121            {darkMode ? 'Light' : 'Dark'}
122          </button>
123        </div>
124        <div>
125          Selected: <strong>{selectedItem || 'None'}</strong>
126        </div>
127      </div>
128
129      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
130        {/* useMemo Demo */}
131        <section style={{
132          padding: '20px',
133          backgroundColor: darkMode ? '#252545' : '#fff',
134          borderRadius: '12px'
135        }}>
136          <h2 style={{ color: '#4caf50' }}>useMemo</h2>
137          <p>Array sum: <strong>{sum}</strong></p>
138          <p style={{ fontSize: '14px', color: '#888' }}>
139            Computations performed only when the 'numbers' array changes.
140            Click "Count" - the sum won't be recalculated!
141          </p>
142          <pre style={{
143            backgroundColor: darkMode ? '#1a1a2e' : '#f5f5f5',
144            padding: '10px',
145            borderRadius: '8px',
146            fontSize: '12px',
147            overflow: 'auto'
148          }}>
149{`const sum = useMemo(() => {
150  return numbers.reduce((a, b) => a + b, 0);
151}, [numbers]);`}
152          </pre>
153        </section>
154
155        {/* React.memo Demo */}
156        <section style={{
157          padding: '20px',
158          backgroundColor: darkMode ? '#252545' : '#fff',
159          borderRadius: '12px'
160        }}>
161          <h2 style={{ color: '#2196f3' }}>React.memo + useCallback</h2>
162          <p style={{ fontSize: '14px', color: '#888', marginBottom: '15px' }}>
163            Check the console! Optimized elements don't re-render when count changes.
164          </p>
165
166          <h4>Without optimization:</h4>
167          {items.slice(0, 2).map(item => (
168            <UnoptimizedItem
169              key={item}
170              name={item}
171              onSelect={(name) => setSelectedItem(name)}
172            />
173          ))}
174
175          <h4 style={{ marginTop: '20px' }}>Z React.memo + useCallback:</h4>
176          {items.slice(0, 2).map(item => (
177            <OptimizedItem
178              key={item}
179              name={item}
180              onSelect={handleSelect}
181            />
182          ))}
183        </section>
184      </div>
185
186      <section style={{
187        marginTop: '20px',
188        padding: '20px',
189        backgroundColor: darkMode ? '#252545' : '#fff',
190        borderRadius: '12px'
191      }}>
192        <h2 style={{ color: '#ff9800' }}>When to optimize?</h2>
193        <ul style={{ lineHeight: '1.8' }}>
194          <li><strong>useMemo:</strong> Expensive computations, creating objects/arrays</li>
195          <li><strong>useCallback:</strong> Callbacks passed to optimized children</li>
196          <li><strong>React.memo:</strong> Components that don't change often</li>
197          <li><strong>Note:</strong> Don't optimize prematurely! Measure first.</li>
198        </ul>
199      </section>
200    </div>
201  );
202}
203
204export default PerformanceDemo;

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What does the useReducer hook return?

Hands-on tasks in the game

  • Click in order

    Build a correct useReducer declaration:

  • Code editor

    Create a research station system

  • Horizontal ordering

    Arrange the correct structure of an action object in useReducer:

  • Vertical ordering

    Arrange the components of a space application architecture from most general to most specific:

  • Code editor

    Create a galactic data bank

Useful articles