JavaScript and React course Β· Module 4: Context API and useReducer
useReducer - advanced state management
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 actioninitialState- 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:
- Syntax:
const [state, dispatch] = useReducer(reducer, initialState) - Reducer: a pure function
(state, action) => newStatewith the switch-case pattern - Dispatch: sending actions
dispatch({ type: 'ACTION', payload: data }) - When to use: complex state, related updates, business logic in state
- 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. 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