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

Project: Galactic Command Center

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

  1. Three independent context systems working simultaneously
  2. The Header component consuming both AuthContext and ThemeContext
  3. Theme changes immediately visible throughout the entire application
  4. Adding missions and changing their status through dispatch
  5. Blocking the mission panel for unauthenticated users
  6. Assigning a ship to a mission (ASSIGN_SHIP action 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?

Useful articles