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.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.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 valueSpace 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!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:
default - return the unchanged state for unknown actionsAn 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});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.
Both hooks serve to manage state, but they have different use cases:
Choose
when:useState
Choose
when:useReducer
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 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.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.
useReducer is a powerful tool for managing complex state in React:const [state, dispatch] = useReducer(reducer, initialState)(state, action) => newState with the switch-case patterndispatch({ type: 'ACTION', payload: data }){} blocks in case, throwing errors in defaultJust 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.