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

State management patterns

7 min read
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

FeatureContext APIRedux ToolkitZustand
Size0 KB (built-in)~11 KB~1.5 KB
BoilerplateMediumLarge (but RTK helps)Minimal
Re-rendersAll consumersOptimized (selectors)Optimized (selectors)
DevToolsNo dedicated onesExtensive (time-travel)Redux DevTools extension
Learning curveEasySteepEasy
Best forTheme, auth, localeLarge apps, large teamsMost applications

The tool selection principle

At Jupiter Station every system has its purpose. The same goes for state management:

  1. Local state (useState) - data needed only by a single component or its direct children
  2. Lifted state - data shared by 2-3 nearby components, move it to their common parent
  3. Context API - global data that changes rarely: theme, auth, locale, language
  4. Zustand - global state in small/medium applications when you need performance and simplicity
  5. 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. 1. When should you use Context API instead of passing props (props drilling)?

  2. 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.

Useful articles