In more complex React applications, state management can become complicated. Sometimes the state update logic grows, operations on state become more complex, and the code becomes harder to understand and maintain. The useReducer hook comes to the rescue, offering a more organized way to manage complex state.
The
useReducer hook is an alternative to useState, particularly useful when we have complex state logic or when the next state depends on the previous one. It is inspired by the Redux pattern and works on similar principles.1const [state, dispatch] = useReducer(reducer, initialState);Where:
state is the current statedispatch is the function used to send actionsreducer is a function that takes the current state and an action, and returns a new stateinitialState is the initial state value1import React, { useReducer } from 'react';
2
3// Reducer function
4function counterReducer(state, action) {
5 switch (action.type) {
6 case 'INCREMENT':
7 return { count: state.count + 1 };
8 case 'DECREMENT':
9 return { count: state.count - 1 };
10 case 'RESET':
11 return { count: 0 };
12 default:
13 return state;
14 }
15}
16
17function CounterWithReducer() {
18 // Initial state
19 const initialState = { count: 0 };
20
21 // Using useReducer
22 const [state, dispatch] = useReducer(counterReducer, initialState);
23
24 return (
25 <div className="counter">
26 <p>Counter: {state.count}</p>
27
28 <button onClick={() => dispatch({ type: 'INCREMENT' })}>
29 Increment
30 </button>
31
32 <button onClick={() => dispatch({ type: 'DECREMENT' })}>
33 Decrement
34 </button>
35
36 <button onClick={() => dispatch({ type: 'RESET' })}>
37 Reset
38 </button>
39 </div>
40 );
41}In this example:
counterReducer function that specifies how the state should change in response to different actionsuseReducer to manage the counter statedispatch function to send actions (objects with a type field) that are processed by the reduceruseReducer truly shines when dealing with complex state where multiple fields can be updated in different ways. Let's consider an example of a spaceship control system:1import React, { useReducer } from 'react';
2
3// Initial state definition
4const initialSpaceshipState = {
5 power: 100,
6 shields: 75,
7 weapons: 0,
8 lifeSupport: 100,
9 status: 'docked',
10 alerts: []
11};
12
13// Reducer function
14function spaceshipReducer(state, action) {
15 switch (action.type) {
16 case 'ALLOCATE_POWER':
17 // Allocate power from the main system to a subsystem
18 return {
19 ...state,
20 power: state.power - action.amount,
21 [action.system]: state[action.system] + action.amount
22 };
23
24 case 'CHANGE_STATUS':
25 return {
26 ...state,
27 status: action.status,
28 // Add alert about status change
29 alerts: [...state.alerts, {
30 id: Date.now(),
31 message: `Status changed to: ${action.status}`,
32 level: 'info'
33 }]
34 };
35
36 case 'POWER_FAILURE':
37 return {
38 ...state,
39 power: Math.max(0, state.power - action.amount),
40 alerts: [...state.alerts, {
41 id: Date.now(),
42 message: `Power failure! Lost ${action.amount}% power`,
43 level: 'critical'
44 }]
45 };
46
47 case 'DISMISS_ALERT':
48 return {
49 ...state,
50 alerts: state.alerts.filter(alert => alert.id !== action.alertId)
51 };
52
53 default:
54 return state;
55 }
56}
57
58function SpaceshipControl() {
59 const [ship, dispatch] = useReducer(spaceshipReducer, initialSpaceshipState);
60
61 const allocatePower = (system, amount) => {
62 if (ship.power >= amount) {
63 dispatch({
64 type: 'ALLOCATE_POWER',
65 system,
66 amount
67 });
68 } else {
69 alert('Insufficient power!');
70 }
71 };
72
73 return (
74 <div className="spaceship-control">
75 <h1>Ship control panel</h1>
76
77 <div className="status-display">
78 <p>Status: {ship.status}</p>
79 <button onClick={() => dispatch({ type: 'CHANGE_STATUS', status: 'orbiting' })}>
80 Enter orbit
81 </button>
82 <button onClick={() => dispatch({ type: 'CHANGE_STATUS', status: 'cruising' })}>
83 Start cruising
84 </button>
85 </div>
86
87 <div className="power-systems">
88 <h2>Onboard systems</h2>
89 <p>Main generator: {ship.power}%</p>
90
91 <div className="system">
92 <p>Shields: {ship.shields}%</p>
93 <button onClick={() => allocatePower('shields', 10)}>
94 Increase power (+10%)
95 </button>
96 </div>
97
98 <div className="system">
99 <p>Weapon systems: {ship.weapons}%</p>
100 <button onClick={() => allocatePower('weapons', 25)}>
101 Charge weapons (+25%)
102 </button>
103 </div>
104
105 <div className="system">
106 <p>Life support system: {ship.lifeSupport}%</p>
107 <button onClick={() => allocatePower('lifeSupport', 5)}>
108 Increase power (+5%)
109 </button>
110 </div>
111
112 <button onClick={() => dispatch({ type: 'POWER_FAILURE', amount: 20 })}>
113 Simulate power failure
114 </button>
115 </div>
116
117 <div className="alerts">
118 <h2>Alert center</h2>
119 {ship.alerts.length === 0 ? (
120 <p>No active alerts</p>
121 ) : (
122 <ul>
123 {ship.alerts.map(alert => (
124 <li key={alert.id} className={alert.level}>
125 {alert.message}
126 <button onClick={() => dispatch({ type: 'DISMISS_ALERT', alertId: alert.id })}>
127 Dismiss
128 </button>
129 </li>
130 ))}
131 </ul>
132 )}
133 </div>
134 </div>
135 );
136}In this more complex example:
One of the advantages of using
useReducer is the ability to better organize and structure the state. Here are some tips for designing state structure:1// Instead of a flat structure
2const initialState = {
3 missionName: 'Apollo',
4 missionStatus: 'planning',
5 crewCount: 3,
6 commanderName: 'Smith',
7 engineStatus: 'check',
8 fuelLevel: 100
9};
10
11// Better to group related data
12const betterInitialState = {
13 mission: {
14 name: 'Apollo',
15 status: 'planning'
16 },
17 crew: {
18 count: 3,
19 commander: 'Smith'
20 },
21 systems: {
22 engine: {
23 status: 'check',
24 fuelLevel: 100
25 }
26 }
27};For collections of objects, it's often worth storing them as "normalized" data:
1// Instead of an array
2const missions = [
3 { id: 1, name: 'Apollo', status: 'completed' },
4 { id: 2, name: 'Artemis', status: 'planning' }
5];
6
7// Better to use an object with IDs as keys
8const normalizedMissions = {
9 byId: {
10 1: { id: 1, name: 'Apollo', status: 'completed' },
11 2: { id: 2, name: 'Artemis', status: 'planning' }
12 },
13 allIds: [1, 2]
14};This makes it easier to update, add, and remove elements without having to iterate through the entire array.
The reducer must always return a new state object, never modify the existing one. For deeply nested objects, libraries like immer can be helpful:
1import produce from 'immer';
2
3function complexReducer(state, action) {
4 switch (action.type) {
5 case 'UPDATE_CREW_MEMBER':
6 // Without immer this would be much more complicated
7 return produce(state, draft => {
8 const member = draft.crew.members.find(m => m.id === action.memberId);
9 if (member) {
10 member.status = action.status;
11 }
12 });
13
14 // other cases...
15
16 default:
17 return state;
18 }
19}Combining Context API with
useReducer is a powerful combination that allows managing global application state without the need for external libraries like Redux:1import React, { createContext, useReducer, useContext } from 'react';
2
3// Create context
4const SpaceshipContext = createContext();
5
6// Reducer
7function spaceshipReducer(state, action) {
8 switch (action.type) {
9 case 'SET_COURSE':
10 return {
11 ...state,
12 destination: action.destination,
13 status: 'en route'
14 };
15 case 'ARRIVE':
16 return {
17 ...state,
18 location: state.destination,
19 destination: null,
20 status: 'arrived'
21 };
22 case 'REFUEL':
23 return {
24 ...state,
25 fuel: 100
26 };
27 default:
28 return state;
29 }
30}
31
32// Provider
33function SpaceshipProvider({ children }) {
34 const initialState = {
35 name: 'USS Enterprise',
36 status: 'docked',
37 location: 'Earth',
38 destination: null,
39 fuel: 75
40 };
41
42 const [state, dispatch] = useReducer(spaceshipReducer, initialState);
43
44 return (
45 <SpaceshipContext.Provider value={{ state, dispatch }}>
46 {children}
47 </SpaceshipContext.Provider>
48 );
49}
50
51// Hook for easy context access
52function useSpaceship() {
53 const context = useContext(SpaceshipContext);
54 if (context === undefined) {
55 throw new Error('useSpaceship must be used within a SpaceshipProvider');
56 }
57 return context;
58}
59
60// Component using context
61function NavigationControl() {
62 const { state, dispatch } = useSpaceship();
63
64 const destinations = ['Mars', 'Jupiter', 'Saturn', 'Alpha Centauri'];
65
66 return (
67 <div className="navigation-control">
68 <h2>Navigation control</h2>
69 <p>Current status: {state.status}</p>
70 <p>Location: {state.location}</p>
71 {state.destination && <p>Destination: {state.destination}</p>}
72
73 <div className="destination-selector">
74 <h3>Select destination:</h3>
75 {destinations.map(dest => (
76 <button
77 key={dest}
78 disabled={state.status === 'en route' || dest === state.location}
79 onClick={() => dispatch({ type: 'SET_COURSE', destination: dest })}
80 >
81 {dest}
82 </button>
83 ))}
84 </div>
85
86 {state.status === 'en route' && (
87 <button onClick={() => dispatch({ type: 'ARRIVE' })}>
88 Simulate arrival at destination
89 </button>
90 )}
91
92 <div className="fuel-control">
93 <p>Fuel level: {state.fuel}%</p>
94 <button
95 onClick={() => dispatch({ type: 'REFUEL' })}
96 disabled={state.status === 'en route' || state.fuel === 100}
97 >
98 Refuel
99 </button>
100 </div>
101 </div>
102 );
103}
104
105// Another component using the same context
106function ShipStatus() {
107 const { state } = useSpaceship();
108
109 return (
110 <div className="ship-status">
111 <h2>{state.name}</h2>
112 <p>Status: <span className={state.status}>{state.status}</span></p>
113 <p>Location: {state.location}</p>
114 {state.destination && <p>Destination: {state.destination}</p>}
115 <div className="fuel-gauge">
116 <div className="fuel-level" style={{ width: `${state.fuel}%` }}>
117 {state.fuel}%
118 </div>
119 </div>
120 </div>
121 );
122}
123
124// Main application
125function App() {
126 return (
127 <SpaceshipProvider>
128 <div className="spaceship-dashboard">
129 <h1>Spaceship control panel</h1>
130 <div className="dashboard-grid">
131 <ShipStatus />
132 <NavigationControl />
133 </div>
134 </div>
135 </SpaceshipProvider>
136 );
137}In this example:
useReducer to manage that stateuseSpaceship for easier context accessNavigationControl and ShipStatus components share the same statedispatch update the state accessible throughout the applicationThis approach gives us many advantages:
As the application becomes more complex, we may need more advanced solutions for global state management. Combining
useReducer with Context API is a good start, but for truly large applications it's worth considering:1function App() {
2 return (
3 <AuthProvider>
4 <MissionProvider>
5 <UIProvider>
6 <NotificationProvider>
7 <MainApplication />
8 </NotificationProvider>
9 </UIProvider>
10 </MissionProvider>
11 </AuthProvider>
12 );
13}1function useReducerWithMiddleware(reducer, initialState, middlewares = []) {
2 const [state, dispatch] = useReducer(reducer, initialState);
3
4 const dispatchWithMiddleware = useCallback(
5 action => {
6 let modifiedAction = { ...action };
7
8 // Apply middleware
9 for (const middleware of middlewares) {
10 middleware(state, modifiedAction);
11 }
12
13 dispatch(modifiedAction);
14 },
15 [state, middlewares]
16 );
17
18 return [state, dispatchWithMiddleware];
19}
20
21// Example logging middleware
22const loggingMiddleware = (state, action) => {
23 console.log('Previous state:', state);
24 console.log('Action:', action);
25};1function MissionProvider({ children }) {
2 const [state, dispatch] = useReducer(missionReducer, initialState);
3
4 // Function handling asynchronous actions
5 const asyncDispatch = useCallback(async (action) => {
6 if (typeof action === 'function') {
7 await action(dispatch, () => state);
8 } else {
9 dispatch(action);
10 }
11 }, [state]);
12
13 return (
14 <MissionContext.Provider value={{ state, dispatch: asyncDispatch }}>
15 {children}
16 </MissionContext.Provider>
17 );
18}
19
20// Usage
21function MissionControls() {
22 const { dispatch } = useMission();
23
24 const startMission = () => {
25 // Asynchronous action
26 dispatch(async (dispatch, getState) => {
27 dispatch({ type: 'MISSION_LOADING' });
28
29 try {
30 const response = await fetch('/api/missions/start');
31 const data = await response.json();
32
33 dispatch({
34 type: 'MISSION_STARTED',
35 payload: data
36 });
37 } catch (error) {
38 dispatch({
39 type: 'MISSION_ERROR',
40 error: error.message
41 });
42 }
43 });
44 };
45
46 return (
47 <button onClick={startMission}>Start mission</button>
48 );
49}As the application scales, it's worth considering more advanced techniques:
1function crewReducer(state, action) {
2 switch (action.type) {
3 case 'ADD_CREW_MEMBER':
4 return [...state, action.member];
5 //
6 default:
7 return state;
8 }
9}
10
11function shipSystemsReducer(state, action) {
12 switch (action.type) {
13 case 'UPDATE_ENGINE_STATUS':
14 return {
15 ...state,
16 engine: {
17 ...state.engine,
18 status: action.status
19 }
20 };
21 //
22 default:
23 return state;
24 }
25}
26
27// Combining reducers
28function rootReducer(state, action) {
29 return {
30 crew: crewReducer(state.crew, action),
31 systems: shipSystemsReducer(state.systems, action),
32 // other parts of state
33 };
34}1// Action creator
2const addCrewMember = (name, role) => ({
3 type: 'ADD_CREW_MEMBER',
4 member: { id: Date.now(), name, role }
5});
6
7// Usage
8dispatch(addCrewMember('Anna Nowak', 'Pilot'));useReducer combined with Context API provides a powerful solution for state management in React applications. It gives structure, predictability, and scalability, while remaining within the React ecosystem without the need to add external dependencies. It is particularly useful for medium and larger applications where state organization becomes a key challenge.