JavaScript and React course Β· Module 10: React Ecosystem and Future
Main Project: Space Mission Command Center
In this lesson6
Congratulations, astronaut! You have reached the final mission in the React Mastery module. It is time to combine all the technologies you have learned into one powerful system - the Space Mission Command Center. This project is your ultimate test to prove you have mastered advanced React patterns.
Project Goal
You will build a dashboard for managing a space mission that combines key concepts from the entire module:
1. React Portals - Modal Alert Windows
Use React Portals to render modal mission alert windows outside the main DOM tree. Alerts should appear above the interface with an overlay effect.
2. Internationalization (i18n)
Add a language switcher (PL/EN) to the header. The entire dashboard should change interface language dynamically using Context API and translation objects.
3. State Management with useReducer
Manage mission state (status, fuel, crew, systems) using useReducer. Define actions such as LAUNCH_MISSION, UPDATE_FUEL, TOGGLE_SYSTEM, ADD_CREW_MEMBER.
4. Compound Components
Create a <Dashboard.Panel> component using the Compound Components pattern that allows flexible composition of dashboard sections:
1<Dashboard>
2 <Dashboard.Header title="Mission Control" />
3 <Dashboard.Panel title="Systems">
4 <SystemsList />
5 </Dashboard.Panel>
6 <Dashboard.Panel title="Crew">
7 <CrewList />
8 </Dashboard.Panel>
9</Dashboard>5. Error Boundaries
Wrap dashboard sections in Error Boundaries so that a failure in one panel does not crash the entire application. Display a friendly error message with an option to reload the section.
6. Performance Optimization
Apply React.memo to list components, useMemo for mission statistics calculations, and useCallback for event handlers passed to child components.
Application Structure
Before you write the first line of code, sketch the component tree. The context providers sit at the very top so that every panel can read the language and the mission state, and every panel has its own Error Boundary, so one failing panel does not take down the others. The alert window renders through a portal, outside the dashboard tree:
1App
2βββ LanguageProvider (Context - i18n)
3βββ MissionProvider (useReducer - state)
4βββ Dashboard (Compound Component)
5β βββ Dashboard.Header
6β βββ Dashboard.Panel "Mission Status"
7β β βββ ErrorBoundary
8β β βββ MissionStatus (React.memo)
9β βββ Dashboard.Panel "Systems"
10β β βββ ErrorBoundary
11β β βββ SystemsList (React.memo)
12β βββ Dashboard.Panel "Crew"
13β βββ ErrorBoundary
14β βββ CrewList (React.memo)
15βββ Portal -> AlertModal (React Portal)Requirements
- Dashboard displays 3 panels: Mission Status, Systems, Crew
- Language switcher (PL/EN) changes all interface labels
- "Launch Mission" button changes status and consumes fuel (useReducer)
- Systems can be toggled on/off
- Alert modal (Portal) appears when fuel drops below 20%
- Error Boundary displays fallback when a component throws an error
- Memoization prevents unnecessary re-renders
Implementation Hints
Reducer for Mission State
The whole mission state lives in one reducer, a pure function that computes the new state from the previous state and an action. It does not modify the state object, it returns a copy with updated fields, so React knows something changed, and React.memo can skip the panels whose data did not change:
1const initialState = {
2 status: 'ready',
3 fuel: 100,
4 systems: [
5 { id: 1, name: 'Navigation', active: true },
6 { id: 2, name: 'Life Support', active: true },
7 { id: 3, name: 'Communications', active: false },
8 ],
9 crew: ['Kowalski', 'Nowak', 'Smith'],
10};
11
12function missionReducer(state, action) {
13 switch (action.type) {
14 case 'LAUNCH_MISSION':
15 return { ...state, status: 'inProgress', fuel: Math.max(0, state.fuel - 20) };
16 case 'UPDATE_FUEL':
17 return { ...state, fuel: Math.max(0, state.fuel + action.payload) };
18 case 'TOGGLE_SYSTEM':
19 return {
20 ...state,
21 systems: state.systems.map(s =>
22 s.id === action.payload ? { ...s, active: !s.active } : s
23 ),
24 };
25 case 'ADD_CREW_MEMBER':
26 return { ...state, crew: [...state.crew, action.payload] };
27 default:
28 return state;
29 }
30}Components do not change the state themselves, they dispatch actions, for example dispatch({ type: 'TOGGLE_SYSTEM', payload: 2 }). Math.max makes sure the fuel never drops below zero.
Context for i18n
Translations are plain objects, one per language. The context provider keeps the current language in state and exposes the setLang function, and components read texts through their own useLanguage hook, so switching the language immediately redraws every label:
1const translations = {
2 pl: { launch: 'Rozpocznij misjΔ', fuel: 'Paliwo', systems: 'Systemy' },
3 en: { launch: 'Launch Mission', fuel: 'Fuel', systems: 'Systems' },
4};
5
6const LanguageContext = createContext();
7
8function LanguageProvider({ children }) {
9 const [lang, setLang] = useState('pl');
10 const t = translations[lang];
11 return (
12 <LanguageContext.Provider value={{ lang, setLang, t }}>
13 {children}
14 </LanguageContext.Provider>
15 );
16}
17
18function useLanguage() {
19 return useContext(LanguageContext);
20}Portal for Alerts
The alert has to cover the whole interface, and a parent with overflow: hidden or its own z-index can clip it. createPortal renders the window into a different DOM node, but events and context still flow through the React tree, so a button in the window can call dispatch or read translations:
1function AlertModal({ message, onClose }) {
2 return createPortal(
3 <div className="modal-overlay">
4 <div className="modal-content">
5 <h2>Alert!</h2>
6 <p>{message}</p>
7 <button onClick={onClose}>Close</button>
8 </div>
9 </div>,
10 document.getElementById('modal-root')
11 );
12}You add the #modal-root node in index.html next to #root. In the editor below the portal simply goes into document.body.
Work Plan
The easiest way is to build the project in layers and check that it works after every step:
- Start with
MissionProviderand its reducer, and one panel that shows the status and the fuel. - Add
LanguageProviderand pass every label through the translation function. - Build
DashboardwithDashboard.HeaderandDashboard.Panel, and put anErrorBoundaryin every panel. - Add the alert in a portal that appears when the fuel drops below 20%.
- Finally, measure renders in React DevTools and add
React.memo,useMemoanduseCallbackwhere they really save work.
Evaluation Criteria
- All 6 patterns (Portals, i18n, useReducer, Compound Components, Error Boundaries, memoization) must be implemented
- The application should work without console errors
- Language switching should update all labels
- The alert should appear automatically when fuel is low
The editor below contains a working skeleton of the command center with all six patterns. Switch the language, launch the mission, burn the fuel below 20% and simulate a failure of the systems panel to see every pattern in action. Then extend the application with your own ideas, for example an action that completes the mission, fleet statistics computed in useMemo or a third language, and compare it with the structure described above.
Code for this lesson: App.jsx
1import { Component, createContext, memo, useCallback, useContext, useMemo, useReducer, useState } from 'react';
2import { createPortal } from 'react-dom';
3import './App.css';
4
5// === I18N: Context with translations ===
6const translations = {
7 pl: {
8 title: 'Centrum Dowodzenia MisjΔ
',
9 status: 'Status misji',
10 systems: 'Systemy',
11 crew: 'ZaΕoga',
12 fuel: 'Paliwo',
13 launch: 'Rozpocznij misjΔ',
14 abort: 'Przerwij misjΔ',
15 burnFuel: '-10% paliwa',
16 ready: 'Gotowi do startu',
17 inProgress: 'Misja w toku',
18 lowFuel: 'Uwaga! Niski poziom paliwa!',
19 alertTitle: 'Alarm',
20 dismiss: 'Zamknij',
21 addMember: 'Dodaj czΕonka',
22 namePlaceholder: 'ImiΔ astronauty',
23 active: 'aktywnych',
24 on: 'WΕ.',
25 off: 'WYΕ.',
26 engine: 'Silnik',
27 shields: 'Tarcze',
28 comms: 'Komunikacja',
29 navigation: 'Nawigacja',
30 commander: 'DowΓ³dca',
31 navigator: 'Nawigator',
32 recruit: 'Rekrut',
33 crashed: 'Panel ulegΕ awarii',
34 reload: 'ZaΕaduj ponownie',
35 simulateCrash: 'Symuluj awariΔ panelu',
36 },
37 en: {
38 title: 'Mission Command Center',
39 status: 'Mission status',
40 systems: 'Systems',
41 crew: 'Crew',
42 fuel: 'Fuel',
43 launch: 'Launch mission',
44 abort: 'Abort mission',
45 burnFuel: '-10% fuel',
46 ready: 'Ready for launch',
47 inProgress: 'Mission in progress',
48 lowFuel: 'Warning! Low fuel level!',
49 alertTitle: 'Alert',
50 dismiss: 'Dismiss',
51 addMember: 'Add member',
52 namePlaceholder: 'Astronaut name',
53 active: 'active',
54 on: 'ON',
55 off: 'OFF',
56 engine: 'Engine',
57 shields: 'Shields',
58 comms: 'Communications',
59 navigation: 'Navigation',
60 commander: 'Commander',
61 navigator: 'Navigator',
62 recruit: 'Recruit',
63 crashed: 'The panel has crashed',
64 reload: 'Reload',
65 simulateCrash: 'Simulate a panel failure',
66 },
67};
68
69const FIRST_LANG = 'en';
70const SECOND_LANG = 'pl';
71
72const LanguageContext = createContext(null);
73
74function LanguageProvider({ children }) {
75 const [lang, setLang] = useState(FIRST_LANG);
76 const t = useCallback(key => translations[lang][key] ?? key, [lang]);
77 const toggleLang = useCallback(() => setLang(l => (l === FIRST_LANG ? SECOND_LANG : FIRST_LANG)), []);
78 const value = useMemo(() => ({ lang, t, toggleLang }), [lang, t, toggleLang]);
79
80 return <LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>;
81}
82
83function useLanguage() {
84 return useContext(LanguageContext);
85}
86
87// === MISSION STATE: useReducer ===
88const initialMission = {
89 status: 'ready',
90 fuel: 100,
91 systems: [
92 { id: 1, key: 'engine', active: true },
93 { id: 2, key: 'shields', active: false },
94 { id: 3, key: 'comms', active: true },
95 { id: 4, key: 'navigation', active: true },
96 ],
97 crew: [
98 { id: 1, name: 'Ra', role: 'commander' },
99 { id: 2, name: 'Astro', role: 'navigator' },
100 ],
101};
102
103function missionReducer(state, action) {
104 switch (action.type) {
105 case 'LAUNCH_MISSION':
106 return { ...state, status: 'inProgress', fuel: Math.max(0, state.fuel - 15) };
107 case 'ABORT_MISSION':
108 return { ...state, status: 'ready' };
109 case 'UPDATE_FUEL':
110 return { ...state, fuel: Math.max(0, state.fuel + action.payload) };
111 case 'TOGGLE_SYSTEM':
112 return {
113 ...state,
114 systems: state.systems.map(s => (s.id === action.payload ? { ...s, active: !s.active } : s)),
115 };
116 case 'ADD_CREW_MEMBER':
117 return { ...state, crew: [...state.crew, { ...action.payload, role: 'recruit' }] };
118 default:
119 return state;
120 }
121}
122
123const MissionContext = createContext(null);
124
125function MissionProvider({ children }) {
126 const [state, dispatch] = useReducer(missionReducer, initialMission);
127 const value = useMemo(() => ({ state, dispatch }), [state]);
128 return <MissionContext.Provider value={value}>{children}</MissionContext.Provider>;
129}
130
131function useMission() {
132 return useContext(MissionContext);
133}
134
135// === ERROR BOUNDARY: one panel failing does not take down the rest ===
136class ErrorBoundary extends Component {
137 constructor(props) {
138 super(props);
139 this.state = { hasError: false };
140 }
141
142 static getDerivedStateFromError() {
143 return { hasError: true };
144 }
145
146 render() {
147 if (this.state.hasError) {
148 return (
149 <div className="error-fallback">
150 <p>{this.props.message}</p>
151 <button onClick={() => this.setState({ hasError: false })}>{this.props.retryLabel}</button>
152 </div>
153 );
154 }
155 return this.props.children;
156 }
157}
158
159// === COMPOUND COMPONENT: Dashboard, Dashboard.Header, Dashboard.Panel ===
160function Dashboard({ children }) {
161 return <div className="dashboard">{children}</div>;
162}
163
164Dashboard.Header = memo(function DashboardHeader() {
165 const { t, lang, toggleLang } = useLanguage();
166 const next = lang === FIRST_LANG ? SECOND_LANG : FIRST_LANG;
167
168 return (
169 <header className="dashboard-header">
170 <h1>{t('title')}</h1>
171 <button className="lang-btn" onClick={toggleLang}>{next.toUpperCase()}</button>
172 </header>
173 );
174});
175
176Dashboard.Panel = memo(function DashboardPanel({ title, children }) {
177 const { t } = useLanguage();
178
179 return (
180 <section className="dashboard-panel">
181 <h2>{title}</h2>
182 <ErrorBoundary message={t('crashed')} retryLabel={t('reload')}>
183 {children}
184 </ErrorBoundary>
185 </section>
186 );
187});
188
189// === MISSION STATUS ===
190const MissionStatus = memo(function MissionStatus() {
191 const { state, dispatch } = useMission();
192 const { t } = useLanguage();
193
194 const handleLaunch = useCallback(() => dispatch({ type: 'LAUNCH_MISSION' }), [dispatch]);
195 const handleAbort = useCallback(() => dispatch({ type: 'ABORT_MISSION' }), [dispatch]);
196 const handleBurn = useCallback(() => dispatch({ type: 'UPDATE_FUEL', payload: -10 }), [dispatch]);
197
198 return (
199 <div>
200 <div className="status-indicator">
201 <span className={'status-dot status-' + state.status} />
202 <span>{t(state.status)}</span>
203 </div>
204 <div className="fuel-bar">
205 <span>{t('fuel')}: {state.fuel}%</span>
206 <div className="fuel-track">
207 <div className={'fuel-fill' + (state.fuel < 20 ? ' fuel-low' : '')} style={{ width: state.fuel + '%' }} />
208 </div>
209 </div>
210 <div className="status-actions">
211 {state.status === 'ready' && <button className="btn-launch" onClick={handleLaunch}>{t('launch')}</button>}
212 {state.status === 'inProgress' && (
213 <>
214 <button className="btn-abort" onClick={handleAbort}>{t('abort')}</button>
215 <button className="btn-fuel" onClick={handleBurn}>{t('burnFuel')}</button>
216 </>
217 )}
218 </div>
219 </div>
220 );
221});
222
223// A sensor that throws an error during rendering on demand (Error Boundary test)
224function SensorArray({ broken }) {
225 if (broken) {
226 throw new Error('Systems sensor failure');
227 }
228 return null;
229}
230
231// === SYSTEMS LIST ===
232const SystemsList = memo(function SystemsList() {
233 const { state, dispatch } = useMission();
234 const { t } = useLanguage();
235 const [broken, setBroken] = useState(false);
236
237 const handleToggle = useCallback(id => dispatch({ type: 'TOGGLE_SYSTEM', payload: id }), [dispatch]);
238 const activeCount = useMemo(() => state.systems.filter(s => s.active).length, [state.systems]);
239
240 return (
241 <div>
242 <SensorArray broken={broken} />
243 <p className="systems-count">{activeCount}/{state.systems.length} {t('active')}</p>
244 {state.systems.map(system => (
245 <div key={system.id} className="system-item">
246 <span>{t(system.key)}</span>
247 <button className={system.active ? 'sys-on' : 'sys-off'} onClick={() => handleToggle(system.id)}>
248 {system.active ? t('on') : t('off')}
249 </button>
250 </div>
251 ))}
252 <button className="btn-crash" onClick={() => setBroken(true)}>{t('simulateCrash')}</button>
253 </div>
254 );
255});
256
257// === CREW LIST ===
258const CrewList = memo(function CrewList() {
259 const { state, dispatch } = useMission();
260 const { t } = useLanguage();
261 const [name, setName] = useState('');
262
263 const handleAdd = useCallback(() => {
264 if (!name.trim()) return;
265 // The id is created in the handler, because the reducer must stay a pure function
266 dispatch({ type: 'ADD_CREW_MEMBER', payload: { id: Date.now(), name: name.trim() } });
267 setName('');
268 }, [name, dispatch]);
269
270 return (
271 <div>
272 {state.crew.map(member => (
273 <div key={member.id} className="crew-item">
274 <strong>{member.name}</strong>
275 <span>{t(member.role)}</span>
276 </div>
277 ))}
278 <div className="crew-add">
279 <input
280 value={name}
281 onChange={e => setName(e.target.value)}
282 onKeyDown={e => e.key === 'Enter' && handleAdd()}
283 placeholder={t('namePlaceholder')}
284 />
285 <button onClick={handleAdd}>{t('addMember')}</button>
286 </div>
287 </div>
288 );
289});
290
291// === ALERT IN A PORTAL: rendered outside the dashboard tree ===
292function AlertModal({ onDismiss }) {
293 const { t } = useLanguage();
294
295 return createPortal(
296 <div className="modal-overlay">
297 <div className="modal-content">
298 <h2>{t('alertTitle')}</h2>
299 <p>{t('lowFuel')}</p>
300 <button onClick={onDismiss}>{t('dismiss')}</button>
301 </div>
302 </div>,
303 document.body
304 );
305}
306
307// === MAIN APPLICATION ===
308function MissionControl() {
309 const { state } = useMission();
310 const { t } = useLanguage();
311 const [dismissedAt, setDismissedAt] = useState(null);
312
313 // The alert is derived from state: low fuel during the mission, unless it was dismissed at this level
314 const showAlert = state.status === 'inProgress' && state.fuel < 20 && dismissedAt !== state.fuel;
315
316 return (
317 <>
318 <Dashboard>
319 <Dashboard.Header />
320 <div className="panels-grid">
321 <Dashboard.Panel title={t('status')}>
322 <MissionStatus />
323 </Dashboard.Panel>
324 <Dashboard.Panel title={t('systems')}>
325 <SystemsList />
326 </Dashboard.Panel>
327 <Dashboard.Panel title={t('crew')}>
328 <CrewList />
329 </Dashboard.Panel>
330 </div>
331 </Dashboard>
332 {showAlert && <AlertModal onDismiss={() => setDismissedAt(state.fuel)} />}
333 </>
334 );
335}
336
337export default function App() {
338 return (
339 <LanguageProvider>
340 <MissionProvider>
341 <MissionControl />
342 </MissionProvider>
343 </LanguageProvider>
344 );
345}Spotted a mistake in this lesson?