JavaScript and React course Β· Module 4: Context API and useReducer
Global state management
In this lesson9
As a React application grows, state management becomes an increasingly complex challenge. In small applications, local component state and props are sufficient, but as things scale, we need more advanced solutions for managing global application state.
In this module we'll look at different approaches to global state management in React, from built-in mechanisms to popular external libraries.
Challenges of global state
Before we dive into solutions, let's understand why global state management can be challenging:
- Data sharing - components in different parts of the component tree may need access to the same data
- Synchronization - changes in one place should be visible everywhere the data is used
- Predictability - state updates should be predictable and easy to trace
- Performance - unnecessary re-rendering should be minimized
- Complexity - state logic can be complicated, with many related changes
Context API + useReducer as a basic solution
As we learned in previous modules, combining Context API with useReducer provides a solid foundation for global state management:
1import React, { createContext, useReducer, useContext } from 'react';
2
3// Initial state
4const initialState = {
5 missions: [],
6 activeMission: null,
7 user: null,
8 isLoading: false,
9 error: null
10};
11
12// Reducer
13function appReducer(state, action) {
14 switch (action.type) {
15 case 'LOGIN_SUCCESS':
16 return { ...state, user: action.payload, error: null };
17 case 'LOGOUT':
18 return { ...state, user: null };
19 case 'FETCH_MISSIONS_START':
20 return { ...state, isLoading: true };
21 case 'FETCH_MISSIONS_SUCCESS':
22 return { ...state, missions: action.payload, isLoading: false };
23 case 'FETCH_MISSIONS_ERROR':
24 return { ...state, error: action.payload, isLoading: false };
25 case 'SET_ACTIVE_MISSION':
26 return { ...state, activeMission: action.payload };
27 default:
28 return state;
29 }
30}
31
32// Context
33const AppStateContext = createContext();
34
35// Provider
36function AppStateProvider({ children }) {
37 const [state, dispatch] = useReducer(appReducer, initialState);
38
39 // We can also add helper functions
40 const login = async (credentials) => {
41 try {
42 dispatch({ type: 'LOGIN_START' });
43 // API call for login
44 const user = await authService.login(credentials);
45 dispatch({ type: 'LOGIN_SUCCESS', payload: user });
46 } catch (error) {
47 dispatch({ type: 'LOGIN_ERROR', payload: error.message });
48 }
49 };
50
51 const fetchMissions = async () => {
52 try {
53 dispatch({ type: 'FETCH_MISSIONS_START' });
54 const missions = await missionService.getAll();
55 dispatch({ type: 'FETCH_MISSIONS_SUCCESS', payload: missions });
56 } catch (error) {
57 dispatch({ type: 'FETCH_MISSIONS_ERROR', payload: error.message });
58 }
59 };
60
61 // Context value
62 const value = {
63 state,
64 dispatch,
65 login,
66 fetchMissions,
67 setActiveMission: (mission) => dispatch({ type: 'SET_ACTIVE_MISSION', payload: mission })
68 };
69
70 return (
71 <AppStateContext.Provider value={value}>
72 {children}
73 </AppStateContext.Provider>
74 );
75}
76
77// Hook for using context
78function useAppState() {
79 const context = useContext(AppStateContext);
80 if (context === undefined) {
81 throw new Error('useAppState must be used within an AppStateProvider');
82 }
83 return context;
84}
85
86// Usage in application
87function App() {
88 return (
89 <AppStateProvider>
90 <Header />
91 <Main />
92 <Footer />
93 </AppStateProvider>
94 );
95}
96
97function Header() {
98 const { state, dispatch } = useAppState();
99
100 return (
101 <header>
102 {state.user ? (
103 <>
104 <span>Welcome, {state.user.name}</span>
105 <button onClick={() => dispatch({ type: 'LOGOUT' })}>Logout</button>
106 </>
107 ) : (
108 <LoginButton />
109 )}
110 </header>
111 );
112}
113
114function MissionsList() {
115 const { state, fetchMissions, setActiveMission } = useAppState();
116
117 useEffect(() => {
118 fetchMissions();
119 }, []);
120
121 if (state.isLoading) return <p>Loading missions...</p>;
122 if (state.error) return <p>Error: {state.error}</p>;
123
124 return (
125 <div>
126 <h2>Available missions</h2>
127 <ul>
128 {state.missions.map(mission => (
129 <li key={mission.id}>
130 {mission.name}
131 <button onClick={() => setActiveMission(mission)}>
132 Select
133 </button>
134 </li>
135 ))}
136 </ul>
137 </div>
138 );
139}This solution has several advantages:
- Uses built-in React mechanisms
- Requires no external dependencies
- Provides good structural foundations for global state
However, as the application grows, this solution may encounter limitations related to performance and code organization.
Scaling applications with multiple contexts
For larger applications, it's recommended to split global state into smaller, independent contexts by domain:
1// Authentication context
2const AuthContext = createContext();
3
4function AuthProvider({ children }) {
5 const [user, setUser] = useState(null);
6 const [isLoading, setIsLoading] = useState(false);
7 const [error, setError] = useState(null);
8
9 const login = async (credentials) => {
10 setIsLoading(true);
11 try {
12 const user = await authService.login(credentials);
13 setUser(user);
14 setError(null);
15 } catch (err) {
16 setError(err.message);
17 } finally {
18 setIsLoading(false);
19 }
20 };
21
22 const logout = async () => {
23 await authService.logout();
24 setUser(null);
25 };
26
27 return (
28 <AuthContext.Provider value={{ user, isLoading, error, login, logout }}>
29 {children}
30 </AuthContext.Provider>
31 );
32}
33
34// Missions context
35const MissionsContext = createContext();
36
37function MissionsProvider({ children }) {
38 const [missions, setMissions] = useReducer(missionsReducer, []);
39 const [activeMission, setActiveMission] = useState(null);
40 // rest of the logic...
41
42 return (
43 <MissionsContext.Provider value={{ missions, activeMission, /*...*/ }}>
44 {children}
45 </MissionsContext.Provider>
46 );
47}
48
49// UI context
50const UIContext = createContext();
51
52function UIProvider({ children }) {
53 const [darkMode, setDarkMode] = useState(false);
54 const [sidebarOpen, setSidebarOpen] = useState(true);
55 // rest of the logic...
56
57 return (
58 <UIContext.Provider value={{ darkMode, toggleDarkMode: () => setDarkMode(!darkMode), /*...*/ }}>
59 {children}
60 </UIContext.Provider>
61 );
62}
63
64// Combining all providers
65function AppProviders({ children }) {
66 return (
67 <AuthProvider>
68 <MissionsProvider>
69 <UIProvider>
70 {children}
71 </UIProvider>
72 </MissionsProvider>
73 </AuthProvider>
74 );
75}Advantages of this approach:
- Better separation of concerns
- Smaller contexts = fewer unnecessary re-renders
- Easier testing of individual modules
- Components can use only the contexts they need
Performance optimization with useMemo and memo
One of the challenges when using Context API is performance - every change in the context value causes a re-render of all components that use that context. Here are some optimization techniques:
1function OptimizedProvider({ children }) {
2 const [state, dispatch] = useReducer(reducer, initialState);
3
4 // Memoize the context value
5 const contextValue = useMemo(() => {
6 return { state, dispatch };
7 }, [state, dispatch]);
8
9 return (
10 <AppContext.Provider value={contextValue}>
11 {children}
12 </AppContext.Provider>
13 );
14}
15
16// Optimizing consuming components
17const MissionItem = memo(function MissionItem({ mission, onSelect }) {
18 return (
19 <li>
20 {mission.name}
21 <button onClick={() => onSelect(mission)}>Select</button>
22 </li>
23 );
24});
25
26function MissionList() {
27 const { missions, selectMission } = useMissions();
28
29 // Memoize callback
30 const handleSelect = useCallback((mission) => {
31 selectMission(mission);
32 }, [selectMission]);
33
34 return (
35 <ul>
36 {missions.map(mission => (
37 <MissionItem
38 key={mission.id}
39 mission={mission}
40 onSelect={handleSelect}
41 />
42 ))}
43 </ul>
44 );
45}Introduction to Redux Toolkit
Redux is a popular state management library that offers a more structured approach to global state. Today, modern Redux means Redux Toolkit (RTK) - the official, recommended toolset that eliminates most of the boilerplate. Instead of manually writing reducers with a switch statement, we use createSlice, and instead of createStore - configureStore. Here are the basics of Redux Toolkit with React:
1import { configureStore, createSlice } from '@reduxjs/toolkit';
2import { Provider, useSelector, useDispatch } from 'react-redux';
3
4// Slice = a piece of state + reducers + actions in one place
5const spaceAppSlice = createSlice({
6 name: 'spaceApp',
7 initialState: {
8 missions: [],
9 user: null
10 },
11 reducers: {
12 // Thanks to Immer we can "mutate" state - RTK does it safely and immutably
13 setUser: (state, action) => {
14 state.user = action.payload;
15 },
16 setMissions: (state, action) => {
17 state.missions = action.payload;
18 }
19 }
20});
21
22// createSlice automatically generates action creators
23export const { setUser, setMissions } = spaceAppSlice.actions;
24
25// Store - configureStore adds DevTools and middleware for you
26const store = configureStore({
27 reducer: spaceAppSlice.reducer
28});
29
30// Provider
31function App() {
32 return (
33 <Provider store={store}>
34 <Main />
35 </Provider>
36 );
37}
38
39// Usage in components
40function UserProfile() {
41 // Selecting part of the state
42 const user = useSelector(state => state.user);
43 const dispatch = useDispatch();
44
45 if (!user) return <p>User not logged in</p>;
46
47 return (
48 <div>
49 <h2>{user.name}</h2>
50 <button onClick={() => dispatch(setUser(null))}>
51 Logout
52 </button>
53 </div>
54 );
55}Historical note: Classic Redux used
createStoreand manual reducers with aswitchstatement. That approach (import { createStore } from 'redux') is now deprecated - the official documentation recommends Redux Toolkit. That is why we learn the modern way from the start.
Code organization in Redux Toolkit
In larger applications, each piece of state is a separate slice. configureStore combines them automatically - you just pass a map of reducers (no manual combineReducers needed):
1// /store/index.js
2import { configureStore } from '@reduxjs/toolkit';
3import authReducer from './auth/authSlice';
4import missionsReducer from './missions/missionsSlice';
5import uiReducer from './ui/uiSlice';
6
7// configureStore combines reducers itself - no combineReducers
8const store = configureStore({
9 reducer: {
10 auth: authReducer,
11 missions: missionsReducer,
12 ui: uiReducer
13 }
14});
15export default store;
16
17// /store/missions/missionsSlice.js
18import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
19
20// Asynchronous operations are handled by createAsyncThunk
21export const fetchMissions = createAsyncThunk(
22 'missions/fetchMissions',
23 async () => {
24 const missions = await api.fetchMissions();
25 return missions;
26 }
27);
28
29const missionsSlice = createSlice({
30 name: 'missions',
31 initialState: {
32 list: [],
33 active: null,
34 loading: false,
35 error: null
36 },
37 reducers: {
38 setActiveMission: (state, action) => {
39 state.active = action.payload;
40 }
41 },
42 // extraReducers react to the thunk states (pending/fulfilled/rejected)
43 extraReducers: (builder) => {
44 builder
45 .addCase(fetchMissions.pending, (state) => {
46 state.loading = true;
47 })
48 .addCase(fetchMissions.fulfilled, (state, action) => {
49 state.list = action.payload;
50 state.loading = false;
51 })
52 .addCase(fetchMissions.rejected, (state, action) => {
53 state.error = action.error.message;
54 state.loading = false;
55 });
56 }
57});
58
59export const { setActiveMission } = missionsSlice.actions;
60export default missionsSlice.reducer;
61
62// /store/missions/selectors.js
63export const selectMissions = state => state.missions.list;
64export const selectActiveMission = state => state.missions.active;
65export const selectMissionsLoading = state => state.missions.loading;Modern alternatives
Many alternative state management libraries have emerged in the React ecosystem that aim to simplify the process and reduce boilerplate code. Here are some popular options:
1. Zustand
Zustand is a minimalist library that combines the simplicity of a hook with the power of a store:
1import create from 'zustand';
2
3// Creating a store
4const useSpaceStore = create((set) => ({
5 missions: [],
6 activeMissionId: null,
7 isLoading: false,
8
9 fetchMissions: async () => {
10 set({ isLoading: true });
11 try {
12 const missions = await api.fetchMissions();
13 set({ missions, isLoading: false });
14 } catch (error) {
15 set({ error: error.message, isLoading: false });
16 }
17 },
18
19 setActiveMission: (id) => set({ activeMissionId: id })
20}));
21
22// Usage in a component
23function MissionPanel() {
24 // Selectively fetching data from the store
25 const missions = useSpaceStore(state => state.missions);
26 const isLoading = useSpaceStore(state => state.isLoading);
27 const fetchMissions = useSpaceStore(state => state.fetchMissions);
28
29 useEffect(() => {
30 fetchMissions();
31 }, [fetchMissions]);
32
33 if (isLoading) return <p>Loading...</p>;
34
35 return (
36 <div>
37 <h2>Missions</h2>
38 <ul>
39 {missions.map(mission => (
40 <MissionItem key={mission.id} mission={mission} />
41 ))}
42 </ul>
43 </div>
44 );
45}2. Recoil
Recoil is a library created by Facebook that introduces the concepts of "atoms" and "selectors":
1import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
2
3// Defining atoms (smallest units of state)
4const missionsState = atom({
5 key: 'missionsState',
6 default: []
7});
8
9const missionsFilterState = atom({
10 key: 'missionsFilterState',
11 default: 'all' // 'all', 'active', 'completed'
12});
13
14// Selector (derived state)
15const filteredMissionsState = selector({
16 key: 'filteredMissionsState',
17 get: ({ get }) => {
18 const missions = get(missionsState);
19 const filter = get(missionsFilterState);
20
21 switch (filter) {
22 case 'active':
23 return missions.filter(mission => mission.status === 'active');
24 case 'completed':
25 return missions.filter(mission => mission.status === 'completed');
26 default:
27 return missions;
28 }
29 }
30});
31
32// Usage in components
33function MissionsFilter() {
34 const [filter, setFilter] = useRecoilState(missionsFilterState);
35
36 return (
37 <div>
38 <button
39 className={filter === 'all' ? 'active' : ''}
40 onClick={() => setFilter('all')}
41 >
42 All
43 </button>
44 <button
45 className={filter === 'active' ? 'active' : ''}
46 onClick={() => setFilter('active')}
47 >
48 Active
49 </button>
50 <button
51 className={filter === 'completed' ? 'active' : ''}
52 onClick={() => setFilter('completed')}
53 >
54 Completed
55 </button>
56 </div>
57 );
58}
59
60function MissionsList() {
61 // Using the filtered value
62 const missions = useRecoilValue(filteredMissionsState);
63
64 return (
65 <ul>
66 {missions.map(mission => (
67 <li key={mission.id}>{mission.name}</li>
68 ))}
69 </ul>
70 );
71}3. Jotai
Jotai, similar to Recoil, uses an atom-based approach but with a more minimalist API:
1import { atom, useAtom } from 'jotai';
2
3// Defining atoms
4const userAtom = atom(null);
5const missionsAtom = atom([]);
6const missionFilterAtom = atom('all');
7
8// Derived atom
9const filteredMissionsAtom = atom(
10 (get) => {
11 const missions = get(missionsAtom);
12 const filter = get(missionFilterAtom);
13
14 switch (filter) {
15 case 'active':
16 return missions.filter(m => m.status === 'active');
17 case 'completed':
18 return missions.filter(m => m.status === 'completed');
19 default:
20 return missions;
21 }
22 }
23);
24
25// Component
26function MissionDashboard() {
27 const [missions, setMissions] = useAtom(missionsAtom);
28 const [filter, setFilter] = useAtom(missionFilterAtom);
29 const [filteredMissions] = useAtom(filteredMissionsAtom);
30
31 // Component logic...
32
33 return (
34 <div>
35 {/* UI */}
36 </div>
37 );
38}Choosing the right solution
How do you choose the best state management solution for your application? Here are some guidelines:
Application size:
- Small applications: useState + useReducer + Context API
- Medium applications: Zustand or Jotai
- Large applications: Redux (with Redux Toolkit) or Recoil
State complexity:
- Simple state: useState
- Moderately complex state: useReducer + Context API
- Complex state with relationships: Redux or Recoil
Performance requirements:
- If rendering performance is critical, consider libraries that provide granular reactivity (Recoil, Jotai)
Team experience:
- If the team knows Redux, it may be worth sticking with it
- For new projects, simpler APIs like Zustand or Jotai can speed up development
Best practices
Regardless of the chosen solution, here are some general best practices for global state management:
Minimize global state - not everything needs to be global; use local state for component-specific data
Split state by logical domains - group related data and logic together
Normalize data - avoid duplication and deep nesting
Maintain immutability - never modify state directly
Document intentions - use descriptive names for actions and selectors
Use selectors - for retrieving derived data
Keep business logic out of components - in actions, reducers, or dedicated hooks
1// Example: Hook with business logic
2function useMissionControl() {
3 const { state, dispatch } = useAppState();
4
5 const launchMission = useCallback(async (missionId) => {
6 // Check business conditions
7 if (!state.user || state.user.role !== 'commander') {
8 throw new Error('Only the commander can start a mission');
9 }
10
11 if (state.weather === 'storm') {
12 throw new Error('Cannot launch during a storm');
13 }
14
15 // Business logic
16 dispatch({ type: 'MISSION_LAUNCH_START', payload: missionId });
17
18 try {
19 await missionService.launch(missionId);
20 dispatch({ type: 'MISSION_LAUNCH_SUCCESS', payload: missionId });
21 } catch (error) {
22 dispatch({ type: 'MISSION_LAUNCH_FAILURE', payload: error.message });
23 throw error;
24 }
25 }, [state.user, state.weather, dispatch]);
26
27 return {
28 missions: state.missions,
29 activeMission: state.activeMission,
30 isLaunching: state.isLaunching,
31 launchError: state.launchError,
32 launchMission
33 };
34}Summary
Global state management is a key aspect of building scalable React applications. In this module we learned about different approaches:
Context API + useReducer - React's built-in solution, good for small and medium applications
Splitting into multiple contexts - a way to organize global state by domain
Performance optimization - techniques for minimizing re-renders
Redux - a traditional library with a structured approach to state management
Modern alternatives - Zustand, Recoil, Jotai offering simpler APIs and less code
Choosing the right solution depends on the application size, state complexity, and team preferences. Regardless of the choice, following best practices will help maintain clean, predictable, and performant code.
Code for this lesson: App.jsx
1import React, { useState, useEffect, useCallback, useRef } from 'react';
2
3// Custom Hook: useLocalStorage
4function useLocalStorage(key, initialValue) {
5 const [storedValue, setStoredValue] = useState(() => {
6 try {
7 const item = window.localStorage.getItem(key);
8 return item ? JSON.parse(item) : initialValue;
9 } catch (error) {
10 return initialValue;
11 }
12 });
13
14 const setValue = (value) => {
15 try {
16 const valueToStore = value instanceof Function ? value(storedValue) : value;
17 setStoredValue(valueToStore);
18 window.localStorage.setItem(key, JSON.stringify(valueToStore));
19 } catch (error) {
20 console.log(error);
21 }
22 };
23
24 return [storedValue, setValue];
25}
26
27// Custom Hook: useToggle
28function useToggle(initialValue = false) {
29 const [value, setValue] = useState(initialValue);
30 const toggle = useCallback(() => setValue(v => !v), []);
31 return [value, toggle];
32}
33
34// Custom Hook: useCounter
35function useCounter(initialValue = 0, step = 1) {
36 const [count, setCount] = useState(initialValue);
37
38 const increment = useCallback(() => setCount(c => c + step), [step]);
39 const decrement = useCallback(() => setCount(c => c - step), [step]);
40 const reset = useCallback(() => setCount(initialValue), [initialValue]);
41
42 return { count, increment, decrement, reset };
43}
44
45// Custom Hook: useFetch
46function useFetch(url) {
47 const [data, setData] = useState(null);
48 const [loading, setLoading] = useState(true);
49 const [error, setError] = useState(null);
50
51 useEffect(() => {
52 const fetchData = async () => {
53 setLoading(true);
54 // Fetch simulation
55 setTimeout(() => {
56 setData({
57 ships: ['Explorer-1', 'Titan-7', 'Guardian-3'],
58 missions: 12,
59 crew: 45
60 });
61 setLoading(false);
62 }, 1500);
63 };
64 fetchData();
65 }, [url]);
66
67 return { data, loading, error };
68}
69
70// Custom Hook: useWindowSize
71function useWindowSize() {
72 const [size, setSize] = useState({
73 width: window.innerWidth,
74 height: window.innerHeight
75 });
76
77 useEffect(() => {
78 const handleResize = () => {
79 setSize({ width: window.innerWidth, height: window.innerHeight });
80 };
81 window.addEventListener('resize', handleResize);
82 return () => window.removeEventListener('resize', handleResize);
83 }, []);
84
85 return size;
86}
87
88// Custom Hook: usePrevious
89function usePrevious(value) {
90 const ref = useRef();
91 useEffect(() => {
92 ref.current = value;
93 }, [value]);
94 return ref.current;
95}
96
97// Demo Component
98function CustomHooksDemo() {
99 // useLocalStorage demo
100 const [name, setName] = useLocalStorage('astronaut-name', 'Cosmonaut');
101
102 // useToggle demo
103 const [isShieldActive, toggleShield] = useToggle(false);
104
105 // useCounter demo
106 const { count: fuel, increment: addFuel, decrement: useFuel, reset: resetFuel } = useCounter(50, 10);
107
108 // useFetch demo
109 const { data, loading, error } = useFetch('/api/station');
110
111 // useWindowSize demo
112 const { width, height } = useWindowSize();
113
114 // usePrevious demo
115 const previousFuel = usePrevious(fuel);
116
117 return (
118 <div style={{
119 fontFamily: 'Arial, sans-serif',
120 padding: '20px',
121 maxWidth: '800px',
122 margin: '0 auto',
123 backgroundColor: '#0a0a1a',
124 minHeight: '100vh',
125 color: 'white'
126 }}>
127 <h1 style={{ textAlign: 'center', color: '#00d4ff' }}>Custom Hooks</h1>
128
129 {/* useLocalStorage */}
130 <section style={{ marginBottom: '30px', padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
131 <h2 style={{ color: '#4caf50' }}>useLocalStorage</h2>
132 <p>Astronaut name (saved in localStorage):</p>
133 <input
134 type="text"
135 value={name}
136 onChange={(e) => setName(e.target.value)}
137 style={{
138 padding: '10px',
139 width: '100%',
140 boxSizing: 'border-box',
141 borderRadius: '8px',
142 border: '1px solid #333',
143 backgroundColor: '#252545',
144 color: 'white'
145 }}
146 />
147 <p style={{ color: '#888', fontSize: '14px' }}>Refresh the page - the value will be preserved!</p>
148 </section>
149
150 {/* useToggle */}
151 <section style={{ marginBottom: '30px', padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
152 <h2 style={{ color: '#ff9800' }}>useToggle</h2>
153 <div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
154 <button
155 onClick={toggleShield}
156 style={{
157 padding: '12px 24px',
158 backgroundColor: isShieldActive ? '#4caf50' : '#f44336',
159 color: 'white',
160 border: 'none',
161 borderRadius: '8px',
162 cursor: 'pointer'
163 }}
164 >
165 {isShieldActive ? 'Shields ACTIVE' : 'Shields OFFLINE'}
166 </button>
167 </div>
168 </section>
169
170 {/* useCounter */}
171 <section style={{ marginBottom: '30px', padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
172 <h2 style={{ color: '#2196f3' }}>useCounter</h2>
173 <div style={{ textAlign: 'center' }}>
174 <div style={{ fontSize: '48px', fontWeight: 'bold', color: fuel < 30 ? '#f44336' : '#4caf50' }}>
175 {fuel}%
176 </div>
177 <p>Fuel (previously: {previousFuel ?? '-'}%)</p>
178 <div style={{ display: 'flex', gap: '10px', justifyContent: 'center' }}>
179 <button onClick={useFuel} style={{ padding: '10px 20px', backgroundColor: '#f44336', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>-10</button>
180 <button onClick={resetFuel} style={{ padding: '10px 20px', backgroundColor: '#666', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>Reset</button>
181 <button onClick={addFuel} style={{ padding: '10px 20px', backgroundColor: '#4caf50', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>+10</button>
182 </div>
183 </div>
184 </section>
185
186 {/* useFetch */}
187 <section style={{ marginBottom: '30px', padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
188 <h2 style={{ color: '#9c27b0' }}>useFetch</h2>
189 {loading ? (
190 <p>Loading station data... β³</p>
191 ) : (
192 <div>
193 <p>Ships: {data?.ships?.join(', ')}</p>
194 <p>Active missions: {data?.missions}</p>
195 <p>Crew members: {data?.crew}</p>
196 </div>
197 )}
198 </section>
199
200 {/* useWindowSize */}
201 <section style={{ padding: '20px', backgroundColor: '#1a1a2e', borderRadius: '12px' }}>
202 <h2 style={{ color: '#e91e63' }}>useWindowSize</h2>
203 <p>Window size: {width} x {height} px</p>
204 <p style={{ color: '#888', fontSize: '14px' }}>Resize the browser window!</p>
205 </section>
206 </div>
207 );
208}
209
210export default CustomHooksDemo;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 is the default context value passed to createContext used?
2. What happens when a component is between two Providers of the same context?
Hands-on tasks in the game
- Vertical ordering
Arrange what happens when the value in a Provider changes:
- Code editor
Implement a space inventory system
- Vertical ordering
Arrange the steps for creating a galactic fleet management system:
- Code editor
Implement a fleet command center