JavaScript and React course Β· Module 10: React Ecosystem and Future

Main Project: Space Mission Command Center

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

  1. Dashboard displays 3 panels: Mission Status, Systems, Crew
  2. Language switcher (PL/EN) changes all interface labels
  3. "Launch Mission" button changes status and consumes fuel (useReducer)
  4. Systems can be toggled on/off
  5. Alert modal (Portal) appears when fuel drops below 20%
  6. Error Boundary displays fallback when a component throws an error
  7. 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:

  1. Start with MissionProvider and its reducer, and one panel that shows the status and the fuel.
  2. Add LanguageProvider and pass every label through the translation function.
  3. Build Dashboard with Dashboard.Header and Dashboard.Panel, and put an ErrorBoundary in every panel.
  4. Add the alert in a portal that appears when the fuel drops below 20%.
  5. Finally, measure renders in React DevTools and add React.memo, useMemo and useCallback where 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?

Useful articles