JavaScript and React course Β· Module 4: Context API and useReducer
State management patterns
In this lesson7
At Jupiter Station you're managing many systems simultaneously - navigation, fuel, crew, communications. Each system needs data and must share it with other modules. In React, state management is one of the most important architectural challenges. Choosing the right pattern determines whether your application will be easy to maintain and scalable, or become a chaotic maze of dependencies.
The problem: Prop drilling
Before you learned about Context API, the only way to pass data down the component tree was prop drilling - manually passing props through every level. It's like sending messages through a chain of crew members, where each one has to repeat the message onward, even though they don't need it themselves.
1// Prop drilling - 4 levels of passing!
2function SpaceStation({ missionData }) {
3 return <Deck missionData={missionData} />;
4}
5
6function Deck({ missionData }) {
7 // Deck doesn't use missionData - just passes it along
8 return <Room missionData={missionData} />;
9}
10
11function Room({ missionData }) {
12 // Room doesn't use it either - passes it along
13 return <Terminal missionData={missionData} />;
14}
15
16function Terminal({ missionData }) {
17 // Only here is the data actually needed!
18 return <p>Mission: {missionData.name}</p>;
19}At 4 levels it's still manageable, but at 8-10 levels (which is normal in large applications) the code becomes unreadable, and every change in data structure requires modifying many intermediate components.
State colocation - keep state close to where it's used
Before reaching for global solutions, consider state colocation - the principle of keeping state as close as possible to the components that need it. Not every piece of data needs to be global. It's like on a spaceship: the cockpit temperature doesn't need to be known in the engine room.
1// BAD: Global state for local data
2function App() {
3 const [searchQuery, setSearchQuery] = useState('');
4 const [isModalOpen, setIsModalOpen] = useState(false);
5 const [formData, setFormData] = useState({});
6 // ... 20 other states
7
8 return (
9 <GlobalContext.Provider value={/* everything */}>
10 <Dashboard />
11 </GlobalContext.Provider>
12 );
13}
14
15// GOOD: State close to where it's used
16function App() {
17 // Only truly global data
18 const [user, setUser] = useState(null);
19 const [theme, setTheme] = useState('dark');
20
21 return (
22 <AppContext.Provider value={{ user, theme }}>
23 <Dashboard />
24 </AppContext.Provider>
25 );
26}
27
28function SearchPanel() {
29 // Local state - only this component needs it
30 const [searchQuery, setSearchQuery] = useState('');
31 return <input value={searchQuery} onChange={e => setSearchQuery(e.target.value)} />;
32}Rule of thumb: if state is used only by a single component or its direct children - keep it local. Move it up (lifting state up) or to context only when distant branches of the component tree need it.
Context API - the built-in solution
Context API solves prop drilling without additional libraries. You learned about it earlier in this module. Its main strengths and limitations:
Strengths:
- Built into React - zero additional dependencies
- Simple API:
createContext,Provider,useContext - Excellent for data that changes rarely (theme, auth, locale)
Limitations:
- Every value change re-renders ALL consumers
- No built-in caching for API data
- Requires combining with useReducer for complex state
- Difficult performance optimization in large applications
1// Context API - good choice for ship configuration
2const ShipConfigContext = createContext();
3
4function ShipConfigProvider({ children }) {
5 const [config, setConfig] = useState({
6 theme: 'dark-nebula',
7 language: 'en',
8 soundEnabled: true,
9 });
10
11 return (
12 <ShipConfigContext.Provider value={{ config, setConfig }}>
13 {children}
14 </ShipConfigContext.Provider>
15 );
16}Redux - command center for large fleets
Redux is a proven state management pattern, inspired by the Flux architecture. It works like the central command center of a fleet - a single store holds the entire application state, and changes happen exclusively through dispatching actions. We write modern Redux with Redux Toolkit - createSlice defines state and reducers, and configureStore builds the store:
1// Redux Toolkit - the modern Redux standard
2import { createSlice, configureStore } from '@reduxjs/toolkit';
3
4// Slice = a piece of state + reducers + actions in one place
5const missionSlice = createSlice({
6 name: 'mission',
7 initialState: {
8 crew: [],
9 fuel: 100,
10 destination: null,
11 status: 'docked',
12 },
13 reducers: {
14 // Immer lets us "mutate" state - RTK turns it into a safe update
15 addCrewMember: (state, action) => {
16 state.crew.push(action.payload);
17 },
18 setDestination: (state, action) => {
19 state.destination = action.payload;
20 },
21 burnFuel: (state, action) => {
22 state.fuel = Math.max(0, state.fuel - action.payload);
23 },
24 launch: (state) => {
25 state.status = 'in-flight';
26 },
27 },
28});
29
30// createSlice automatically generates action creators
31export const { addCrewMember, setDestination, burnFuel, launch } = missionSlice.actions;
32
33// configureStore builds the store (with DevTools and middleware included)
34const store = configureStore({
35 reducer: {
36 mission: missionSlice.reducer,
37 },
38});
39
40// Usage in a component:
41function MissionControl() {
42 const dispatch = useDispatch();
43 const { crew, fuel, status } = useSelector(state => state.mission);
44
45 return (
46 <div>
47 <p>Status: {status} | Fuel: {fuel}% | Crew: {crew.length}</p>
48 <button onClick={() => dispatch(launch())}>Launch!</button>
49 <button onClick={() => dispatch(burnFuel(10))}>Burn fuel</button>
50 </div>
51 );
52}When Redux?
- Applications with many developers (clear rules, predictable data flow)
- Complex state with many related actions
- You need DevTools with time-travel debugging
- Middleware for handling side-effects (thunks, sagas)
When NOT Redux?
- Simple applications with few states
- Small team or solo developer
- Primarily API data (React Query is better)
Zustand - the lightweight maneuvering thruster
Zustand is a minimalist state management library. If Redux is the main engine, Zustand is the agile maneuvering thruster - it does the same thing, but with less overhead. No providers, no boilerplate, no Context API re-render problems.
1// Zustand - simple store without providers
2// import { create } from 'zustand';
3
4// Creating a store - that's all you need!
5// const useShipStore = create((set) => ({
6// fuel: 100,
7// position: { x: 0, y: 0 },
8// crew: 5,
9//
10// burnFuel: (amount) => set((state) => ({
11// fuel: Math.max(0, state.fuel - amount)
12// })),
13//
14// moveTo: (x, y) => set({ position: { x, y } }),
15//
16// addCrew: () => set((state) => ({
17// crew: state.crew + 1
18// })),
19// }));
20
21// Usage in a component - no Provider needed!
22function FuelGauge() {
23 // Component re-renders ONLY when fuel changes
24 // const fuel = useShipStore((state) => state.fuel);
25 // return <div>Fuel: {fuel}%</div>;
26}
27
28function NavigationPanel() {
29 // Reads ONLY position - a fuel change doesn't cause a re-render
30 // const position = useShipStore((state) => state.position);
31 // const moveTo = useShipStore((state) => state.moveTo);
32 // return <button onClick={() => moveTo(10, 20)}>Fly!</button>;
33}Zustand automatically optimizes re-renders thanks to selectors. Each component subscribes only to the data it actually uses. A fuel change doesn't re-render the navigation panel - and vice versa.
Pattern comparison - galaxy map
| Feature | Context API | Redux Toolkit | Zustand |
|---|---|---|---|
| Size | 0 KB (built-in) | ~11 KB | ~1.5 KB |
| Boilerplate | Medium | Large (but RTK helps) | Minimal |
| Re-renders | All consumers | Optimized (selectors) | Optimized (selectors) |
| DevTools | No dedicated ones | Extensive (time-travel) | Redux DevTools extension |
| Learning curve | Easy | Steep | Easy |
| Best for | Theme, auth, locale | Large apps, large teams | Most applications |
The tool selection principle
At Jupiter Station every system has its purpose. The same goes for state management:
- Local state (
useState) - data needed only by a single component or its direct children - Lifted state - data shared by 2-3 nearby components, move it to their common parent
- Context API - global data that changes rarely: theme, auth, locale, language
- Zustand - global state in small/medium applications when you need performance and simplicity
- Redux Toolkit - large applications with many developers, complex business logic, need for DevTools
Remember: there is no single best solution. The best space engineers match the tool to the problem, not the problem to the tool.
Test the pattern comparison in the editor below:
Code for this lesson: App.jsx
1import React, { createContext, useContext, useState, useReducer } from 'react';
2
3// === STATE MANAGEMENT PATTERNS ===
4
5// Demo: Comparison of state management patterns
6// 1. Context API - built into React
7const ThemeContext = React.createContext();
8
9function ThemeProvider({ children }) {
10 const [theme, setTheme] = useState('dark-nebula');
11 const toggleTheme = () => setTheme(t => t === 'dark-nebula' ? 'light-star' : 'dark-nebula');
12 return (
13 <ThemeContext.Provider value={{ theme, toggleTheme }}>
14 {children}
15 </ThemeContext.Provider>
16 );
17}
18
19function useTheme() {
20 const ctx = useContext(ThemeContext);
21 if (!ctx) throw new Error('useTheme must be inside ThemeProvider');
22 return ctx;
23}
24
25// 2. useReducer - Redux-like pattern built into React
26const missionReducer = (state, action) => {
27 switch (action.type) {
28 case 'ADD_CREW':
29 return { ...state, crew: [...state.crew, action.payload] };
30 case 'SET_DESTINATION':
31 return { ...state, destination: action.payload };
32 case 'BURN_FUEL':
33 return { ...state, fuel: Math.max(0, state.fuel - action.payload) };
34 case 'LAUNCH':
35 return { ...state, status: 'in-flight' };
36 case 'DOCK':
37 return { ...state, status: 'docked', fuel: 100 };
38 default:
39 return state;
40 }
41};
42
43const initialMission = {
44 crew: ['Captain Ra', 'Navigator Nova'],
45 fuel: 100,
46 destination: null,
47 status: 'docked',
48};
49
50// === DEMO PANELS ===
51
52function ContextDemo() {
53 const { theme, toggleTheme } = useTheme();
54 const isDark = theme === 'dark-nebula';
55 return (
56 <div style={{ ...styles.panel, background: isDark ? '#1a2035' : '#e8eaf6' }}>
57 <h3 style={{ color: '#4fc3f7' }}>Context API</h3>
58 <p style={{ color: isDark ? '#ccc' : '#333', fontSize: 13 }}>
59 Theme: <strong>{theme}</strong>
60 </p>
61 <p style={{ color: isDark ? '#888' : '#666', fontSize: 11 }}>
62 Ideal for: theme, auth, locale
63 </p>
64 <button style={styles.btn} onClick={toggleTheme}>Change theme</button>
65 </div>
66 );
67}
68
69function ReducerDemo() {
70 const [state, dispatch] = useReducer(missionReducer, initialMission);
71 const destinations = ['Mars', 'Europa', 'Titan', 'Kepler-442b'];
72
73 return (
74 <div style={styles.panel}>
75 <h3 style={{ color: '#ffd700' }}>useReducer (Redux-like)</h3>
76 <div style={styles.stats}>
77 <div>Status: <span style={{ color: state.status === 'in-flight' ? '#69f0ae' : '#ffd700' }}>{state.status}</span></div>
78 <div>Fuel: <span style={{ color: state.fuel < 30 ? '#ff5252' : '#69f0ae' }}>{state.fuel}%</span></div>
79 <div>Destination: <span style={{ color: '#4fc3f7' }}>{state.destination || 'none'}</span></div>
80 <div>Crew: {state.crew.join(', ')}</div>
81 </div>
82 <div style={styles.btnRow}>
83 {state.status === 'docked' ? (
84 <>
85 <button style={styles.btn} onClick={() => {
86 const dest = destinations[Math.floor(Math.random() * destinations.length)];
87 dispatch({ type: 'SET_DESTINATION', payload: dest });
88 }}>Choose destination</button>
89 {state.destination && (
90 <button style={{ ...styles.btn, background: 'rgba(105,240,174,0.2)', borderColor: '#69f0ae' }}
91 onClick={() => dispatch({ type: 'LAUNCH' })}>
92 Start!
93 </button>
94 )}
95 </>
96 ) : (
97 <>
98 <button style={styles.btn} onClick={() => dispatch({ type: 'BURN_FUEL', payload: 15 })}>
99 Engines (-15)
100 </button>
101 <button style={{ ...styles.btn, background: 'rgba(255,215,0,0.2)' }}
102 onClick={() => dispatch({ type: 'DOCK' })}>
103 Dock
104 </button>
105 </>
106 )}
107 </div>
108 </div>
109 );
110}
111
112function ColocationDemo() {
113 // Local state - doesn't need Context or Redux
114 const [localSearch, setLocalSearch] = useState('');
115 const [results, setResults] = useState([]);
116
117 const planets = ['Mars', 'Jupiter', 'Saturn', 'Neptune', 'Venus', 'Mercury', 'Uranus', 'Pluto'];
118
119 const handleSearch = (query) => {
120 setLocalSearch(query);
121 if (query.length > 0) {
122 setResults(planets.filter(p => p.toLowerCase().includes(query.toLowerCase())));
123 } else {
124 setResults([]);
125 }
126 };
127
128 return (
129 <div style={styles.panel}>
130 <h3 style={{ color: '#ce93d8' }}>State Colocation</h3>
131 <p style={{ color: '#888', fontSize: 11, marginBottom: 8 }}>
132 Local state - without Context/Redux
133 </p>
134 <input
135 style={styles.input}
136 placeholder="Search for a planet..."
137 value={localSearch}
138 onChange={e => handleSearch(e.target.value)}
139 />
140 {results.length > 0 && (
141 <div style={styles.resultList}>
142 {results.map(r => (
143 <div key={r} style={styles.resultItem}>{r}</div>
144 ))}
145 </div>
146 )}
147 </div>
148 );
149}
150
151function ComparisonTable() {
152 const data = [
153 { pattern: 'useState', size: '0 KB', reRenders: 'Local', bestFor: 'Single component state' },
154 { pattern: 'Context API', size: '0 KB', reRenders: 'All consumers', bestFor: 'Theme, auth, locale' },
155 { pattern: 'useReducer', size: '0 KB', reRenders: 'Local', bestFor: 'Complex state logic' },
156 { pattern: 'Redux (RTK)', size: '~11 KB', reRenders: 'Selective', bestFor: 'Large apps, teams' },
157 { pattern: 'Zustand', size: '~1.5 KB', reRenders: 'Selective', bestFor: 'Most applications' },
158 ];
159
160 return (
161 <div style={{ ...styles.panel, gridColumn: '1 / -1' }}>
162 <h3 style={{ color: '#ffd700', textAlign: 'center' }}>Pattern comparison</h3>
163 <div style={styles.table}>
164 <div style={styles.tableHeader}>
165 <span>Pattern</span><span>Size</span><span>Re-renders</span><span>Best for</span>
166 </div>
167 {data.map(row => (
168 <div key={row.pattern} style={styles.tableRow}>
169 <span style={{ color: '#4fc3f7' }}>{row.pattern}</span>
170 <span>{row.size}</span>
171 <span>{row.reRenders}</span>
172 <span style={{ color: '#888' }}>{row.bestFor}</span>
173 </div>
174 ))}
175 </div>
176 </div>
177 );
178}
179
180function App() {
181 return (
182 <ThemeProvider>
183 <div style={styles.container}>
184 <h1 style={styles.title}>State Management Patterns</h1>
185 <p style={styles.subtitle}>
186 Comparison of Context API, useReducer and state colocation
187 </p>
188 <div style={styles.grid}>
189 <ContextDemo />
190 <ReducerDemo />
191 <ColocationDemo />
192 <ComparisonTable />
193 </div>
194 </div>
195 </ThemeProvider>
196 );
197}
198
199const styles = {
200 container: {
201 fontFamily: 'Arial, sans-serif',
202 background: 'linear-gradient(135deg, #0a0e1a 0%, #1a1a3e 100%)',
203 minHeight: '100vh',
204 padding: 24,
205 color: 'white',
206 },
207 title: { textAlign: 'center', color: '#ffd700', marginBottom: 4 },
208 subtitle: { textAlign: 'center', color: '#888', fontSize: 13, marginBottom: 24 },
209 grid: {
210 display: 'grid',
211 gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
212 gap: 16,
213 maxWidth: 900,
214 margin: '0 auto',
215 },
216 panel: {
217 background: '#1a2035',
218 borderRadius: 10,
219 padding: 16,
220 border: '1px solid rgba(79,195,247,0.2)',
221 },
222 stats: {
223 fontSize: 13,
224 lineHeight: 1.8,
225 marginBottom: 12,
226 padding: '8px 12px',
227 background: 'rgba(0,0,0,0.3)',
228 borderRadius: 8,
229 },
230 btnRow: { display: 'flex', gap: 8, justifyContent: 'center', flexWrap: 'wrap' },
231 btn: {
232 padding: '6px 16px',
233 background: 'rgba(255,255,255,0.08)',
234 color: 'white',
235 border: '1px solid rgba(255,255,255,0.15)',
236 borderRadius: 6,
237 cursor: 'pointer',
238 fontSize: 13,
239 },
240 input: {
241 width: '100%',
242 padding: '8px 12px',
243 background: 'rgba(0,0,0,0.3)',
244 color: 'white',
245 border: '1px solid rgba(79,195,247,0.3)',
246 borderRadius: 6,
247 fontSize: 13,
248 outline: 'none',
249 boxSizing: 'border-box',
250 },
251 resultList: {
252 marginTop: 8,
253 background: 'rgba(0,0,0,0.2)',
254 borderRadius: 6,
255 overflow: 'hidden',
256 },
257 resultItem: {
258 padding: '6px 12px',
259 borderBottom: '1px solid rgba(255,255,255,0.05)',
260 fontSize: 13,
261 color: '#ce93d8',
262 },
263 table: { fontSize: 12 },
264 tableHeader: {
265 display: 'grid',
266 gridTemplateColumns: '1fr 0.7fr 1fr 1.3fr',
267 gap: 8,
268 padding: '8px 0',
269 borderBottom: '1px solid rgba(255,215,0,0.3)',
270 fontWeight: 'bold',
271 color: '#ffd700',
272 },
273 tableRow: {
274 display: 'grid',
275 gridTemplateColumns: '1fr 0.7fr 1fr 1.3fr',
276 gap: 8,
277 padding: '6px 0',
278 borderBottom: '1px solid rgba(255,255,255,0.05)',
279 color: '#ccc',
280 },
281};
282
283export default App;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. When should you use Context API instead of passing props (props drilling)?
2. What happens when one large context stores the ship position (updated 60x/s) and the fuel level, and a FuelGauge component reads only fuel?
These are 2 of 5 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create three separate contexts (FuelContext, PositionContext, CrewContext) with their own providers. Compose them together in ShipProviders. Display data from each context in separate components.
- Click in order
Arrange the steps for creating a custom hook for safe context access:
- Vertical ordering
Arrange the stages of building a separated context system from outside to inside:
- Code editor
Implement a custom hook useFuelContext with context availability validation. Create a FuelProvider and a FuelPanel component that uses the hook.