JavaScript and React course Β· Module 4: Context API and useReducer
Project: Galactic Command Center
In this lesson5
Throughout this module you've been building knowledge step by step: Context API, reducers, combining these tools, optimization. Now it's time to bring it all together in one project - the Galactic Command Center.
This is not another theoretical lesson. This is a project that you'll build yourself, using everything you've learned.
What will you build?
An application managing a fleet of spaceships with three independent systems, each with its own context:
- ThemeContext - a theme toggle (daytime mode "Sunrise" / nighttime mode "Deep Space")
- AuthContext - an authorization system (Captain logged in / logged out)
- MissionContext - the heart of the application: mission management via useReducer
Components on screen will consume different contexts depending on their needs.
Project requirements
1. ThemeContext
The context stores the current theme and a toggle function. The ThemeToggle component changes the theme, and the rest of the application reacts to the change (e.g., a CSS class on the main container).
1// Expected context structure
2{
3 theme: 'dark' | 'light',
4 toggleTheme: () => void
5}2. AuthContext
The context stores captain data and enables login/logout. The login form should accept the captain's name. After logging in, display a greeting and a logout button.
1// Expected context structure
2{
3 captain: { name: string } | null,
4 login: (name: string) => void,
5 logout: () => void
6}3. MissionContext with useReducer
This is the main part of the project. The reducer handles at least 4 action types:
1// Action types to implement
2'ADD_MISSION' // adds a new mission to the list
3'UPDATE_STATUS' // changes the status of an existing mission
4'ASSIGN_SHIP' // assigns a ship to a mission
5'COMPLETE_MISSION' // marks a mission as completed
6
7// Example state shape
8{
9 missions: [
10 {
11 id: 1,
12 name: 'Mars Exploration',
13 status: 'planning' | 'active' | 'completed',
14 assignedShip: string | null
15 }
16 ],
17 availableShips: ['USS Nova', 'Orion-7', 'Celestia']
18}Component structure
The application should consist of the following components:
1function App() {
2 return (
3 <ThemeProvider>
4 <AuthProvider>
5 <MissionProvider>
6 <div className="app">
7 <Header /> {/* uses AuthContext + ThemeContext */}
8 <MissionDashboard /> {/* uses MissionContext + AuthContext */}
9 <ThemeToggle /> {/* uses ThemeContext */}
10 </div>
11 </MissionProvider>
12 </AuthProvider>
13 </ThemeProvider>
14 );
15}Header - displays the application title, login status (from AuthContext), and the current theme.
MissionDashboard - mission list, form for adding a new mission, assigning ships. Accessible only to logged-in captains.
ThemeToggle - a button that toggles between daytime and nighttime modes.
Implementation tips
Start by building each provider separately and testing it before combining them all. A good order is: ThemeProvider (simplest) -> AuthProvider -> MissionProvider.
Remember custom hooks for each context:
1function useTheme() {
2 const ctx = useContext(ThemeContext);
3 if (!ctx) throw new Error('useTheme must be inside ThemeProvider');
4 return ctx;
5}
6
7function useAuth() {
8 const ctx = useContext(AuthContext);
9 if (!ctx) throw new Error('useAuth must be inside AuthProvider');
10 return ctx;
11}
12
13function useMissions() {
14 const ctx = useContext(MissionContext);
15 if (!ctx) throw new Error('useMissions must be inside MissionProvider');
16 return ctx;
17}For MissionContext, apply the pattern of splitting into a state context and a dispatch context that you learned in the previous lesson - this is good practice with complex state using useReducer.
What to demonstrate in the project
The completed project should show:
- Three independent context systems working simultaneously
- The
Headercomponent consuming bothAuthContextandThemeContext - Theme changes immediately visible throughout the entire application
- Adding missions and changing their status through dispatch
- Blocking the mission panel for unauthenticated users
- Assigning a ship to a mission (
ASSIGN_SHIPaction with payload)
The project is intentionally described broadly - this is your chance to take a creative approach to the solution. Use the editor below to build your version of the Galactic Command Center.
Code for this lesson: App.jsx
1import React, { createContext, useContext, useReducer, useState } from 'react';
2
3// ============================================================
4// PROJECT: Galactic Command Center
5// Task: Complete the missing code fragments (marked with TODO)
6// ============================================================
7
8// --- 1. THEME CONTEXT ---
9const ThemeContext = createContext();
10
11function ThemeProvider({ children }) {
12 const [darkMode, setDarkMode] = useState(true);
13 // TODO: Also add the toggleTheme function to value
14 return (
15 <ThemeContext.Provider value={{ darkMode, toggleTheme: () => setDarkMode(d => !d) }}>
16 {children}
17 </ThemeContext.Provider>
18 );
19}
20
21function useTheme() {
22 // TODO: Use useContext to read ThemeContext
23 return useContext(ThemeContext);
24}
25
26// --- 2. AUTH CONTEXT ---
27const AuthContext = createContext();
28
29const authInitialState = { user: null, isLoggedIn: false };
30
31function authReducer(state, action) {
32 switch (action.type) {
33 case 'LOGIN':
34 // TODO: Return new state with user: action.user and isLoggedIn: true
35 return { ...state, user: action.user, isLoggedIn: true };
36 case 'LOGOUT':
37 return authInitialState;
38 default:
39 return state;
40 }
41}
42
43function AuthProvider({ children }) {
44 const [state, dispatch] = useReducer(authReducer, authInitialState);
45 return (
46 <AuthContext.Provider value={{ ...state, dispatch }}>
47 {children}
48 </AuthContext.Provider>
49 );
50}
51
52function useAuth() { return useContext(AuthContext); }
53
54// --- 3. MISSION CONTEXT ---
55const MissionContext = createContext();
56
57const missionInitialState = { missions: [] };
58
59function missionReducer(state, action) {
60 switch (action.type) {
61 case 'ADD_MISSION':
62 // TODO: Add new mission to the missions array
63 // Mission has fields: id, name, status: 'pending', createdBy
64 return {
65 ...state,
66 missions: [
67 ...state.missions,
68 { id: Date.now(), name: action.name, status: 'pending', createdBy: action.createdBy },
69 ],
70 };
71 case 'UPDATE_STATUS':
72 return {
73 ...state,
74 missions: state.missions.map(m =>
75 m.id === action.id ? { ...m, status: action.status } : m
76 ),
77 };
78 default:
79 return state;
80 }
81}
82
83function MissionProvider({ children }) {
84 const [state, dispatch] = useReducer(missionReducer, missionInitialState);
85 // TODO: Wrap children in MissionContext.Provider with value={{ ...state, dispatch }}
86 return (
87 <MissionContext.Provider value={{ ...state, dispatch }}>
88 {children}
89 </MissionContext.Provider>
90 );
91}
92
93function useMission() { return useContext(MissionContext); }
94
95// --- UI COMPONENTS ---
96
97function LoginPanel() {
98 const { isLoggedIn, user, dispatch } = useAuth();
99 const [name, setName] = useState('');
100 if (isLoggedIn) {
101 return (
102 <div style={panel}>
103 <span style={{ color: '#4fc3f7' }}>Logged in: <b>{user}</b></span>
104 <button onClick={() => dispatch({ type: 'LOGOUT' })} style={{ ...btn, marginLeft: 12 }}>
105 Log out
106 </button>
107 </div>
108 );
109 }
110 return (
111 <div style={panel}>
112 <input
113 value={name} onChange={e => setName(e.target.value)}
114 placeholder="Your name (pilot)..." style={input}
115 />
116 <button
117 onClick={() => { if (name.trim()) dispatch({ type: 'LOGIN', user: name.trim() }); }}
118 style={{ ...btn, background: '#4fc3f7', color: '#0a0e1a' }}
119 >
120 Log in
121 </button>
122 </div>
123 );
124}
125
126function MissionPanel() {
127 const { isLoggedIn, user } = useAuth();
128 const { missions, dispatch } = useMission();
129 const [newMission, setNewMission] = useState('');
130 const statusLabels = { pending: 'β³ Pending', active: 'Active', done: 'Completed' };
131 const statusColors = { pending: '#ff9800', active: '#4fc3f7', done: '#4caf50' };
132
133 if (!isLoggedIn) {
134 return (
135 <div style={{ ...panel, color: '#778da9', textAlign: 'center' }}>
136 Log in to manage missions
137 </div>
138 );
139 }
140
141 return (
142 <div style={panel}>
143 <h3 style={{ color: '#4fc3f7', marginTop: 0 }}>Space missions</h3>
144 <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
145 <input
146 value={newMission} onChange={e => setNewMission(e.target.value)}
147 placeholder="Mission name..." style={{ ...input, flex: 1 }}
148 />
149 <button
150 onClick={() => {
151 // TODO: Call dispatch with type: 'ADD_MISSION', name: newMission, createdBy: user
152 if (newMission.trim()) {
153 dispatch({ type: 'ADD_MISSION', name: newMission.trim(), createdBy: user });
154 setNewMission('');
155 }
156 }}
157 style={{ ...btn, background: '#4fc3f7', color: '#0a0e1a' }}
158 >
159 Add
160 </button>
161 </div>
162 {missions.length === 0 && (
163 <p style={{ color: '#555', textAlign: 'center' }}>No missions. Add the first one!</p>
164 )}
165 {missions.map(m => (
166 <div key={m.id} style={{ ...missionRow }}>
167 <div>
168 <div style={{ fontWeight: 'bold' }}>{m.name}</div>
169 <div style={{ fontSize: 11, color: '#778da9' }}>Pilot: {m.createdBy}</div>
170 </div>
171 <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
172 <span style={{ color: statusColors[m.status], fontSize: 12 }}>
173 {statusLabels[m.status]}
174 </span>
175 {m.status === 'pending' && (
176 <button onClick={() => dispatch({ type: 'UPDATE_STATUS', id: m.id, status: 'active' })} style={btn}>
177 Activate
178 </button>
179 )}
180 {m.status === 'active' && (
181 <button onClick={() => dispatch({ type: 'UPDATE_STATUS', id: m.id, status: 'done' })} style={btn}>
182 Complete
183 </button>
184 )}
185 </div>
186 </div>
187 ))}
188 </div>
189 );
190}
191
192function ThemeToggle() {
193 const { darkMode, toggleTheme } = useTheme();
194 return (
195 <button onClick={toggleTheme} style={{ ...btn, float: 'right', marginBottom: 12 }}>
196 {darkMode ? 'Light theme' : 'Dark theme'}
197 </button>
198 );
199}
200
201// --- MAIN APPLICATION ---
202// TODO: Wrap App in ThemeProvider, AuthProvider and MissionProvider (order matters!)
203function App() {
204 const { darkMode } = useTheme();
205 const bg = darkMode ? '#0a0e1a' : '#e8f4fd';
206 const fg = darkMode ? 'white' : '#0a0e1a';
207
208 return (
209 <div style={{ minHeight: '100vh', background: bg, padding: 24, fontFamily: 'Arial', color: fg, transition: 'all 0.3s' }}>
210 <ThemeToggle />
211 <h1 style={{ textAlign: 'center', color: '#ffd700', marginBottom: 24, clear: 'both' }}>
212 Galactic Command Center
213 </h1>
214 <LoginPanel />
215 <MissionPanel />
216 </div>
217 );
218}
219
220function WrappedApp() {
221 return (
222 <ThemeProvider>
223 <AuthProvider>
224 <MissionProvider>
225 <App />
226 </MissionProvider>
227 </AuthProvider>
228 </ThemeProvider>
229 );
230}
231
232// Style
233const panel = {
234 background: '#1a2035', borderRadius: 10, padding: 16, marginBottom: 14,
235 border: '1px solid rgba(79,195,247,0.2)',
236};
237const btn = {
238 background: 'rgba(255,255,255,0.08)', color: 'white',
239 border: '1px solid rgba(255,255,255,0.2)', borderRadius: 6,
240 padding: '5px 12px', cursor: 'pointer', fontSize: 12,
241};
242const input = {
243 background: '#0a0e1a', color: 'white',
244 border: '1px solid rgba(79,195,247,0.3)', borderRadius: 6,
245 padding: '6px 10px', boxSizing: 'border-box',
246};
247const missionRow = {
248 display: 'flex', justifyContent: 'space-between', alignItems: 'center',
249 padding: '8px 0', borderBottom: '1px solid rgba(255,255,255,0.07)',
250};
251
252export { WrappedApp as default };Spotted a mistake in this lesson?