At Jupiter Station you're managing many systems simultaneously - navigation, fuel, crew, communications. Each system needs data and must share it with other modules. In React, state management is one of the most important architectural challenges. Choosing the right pattern determines whether your application will be easy to maintain and scalable, or become a chaotic maze of dependencies.
Before you learned about Context API, the only way to pass data down the component tree was
prop drilling - manually passing props through every level. It's like sending messages through a chain of crew members, where each one has to repeat the message onward, even though they don't need it themselves.1// Prop drilling - 4 levels of passing!
2function SpaceStation({ missionData }) {
3 return <Deck missionData={missionData} />;
4}
5
6function Deck({ missionData }) {
7 // Deck doesn't use missionData - just passes it along
8 return <Room missionData={missionData} />;
9}
10
11function Room({ missionData }) {
12 // Room doesn't use it either - passes it along
13 return <Terminal missionData={missionData} />;
14}
15
16function Terminal({ missionData }) {
17 // Only here is the data actually needed!
18 return <p>Mission: {missionData.name}</p>;
19}At 4 levels it's still manageable, but at 8-10 levels (which is normal in large applications) the code becomes unreadable, and every change in data structure requires modifying many intermediate components.
Before reaching for global solutions, consider state colocation - the principle of keeping state as close as possible to the components that need it. Not every piece of data needs to be global. It's like on a spaceship: the cockpit temperature doesn't need to be known in the engine room.
1// BAD: Global state for local data
2function App() {
3 const [searchQuery, setSearchQuery] = useState('');
4 const [isModalOpen, setIsModalOpen] = useState(false);
5 const [formData, setFormData] = useState({});
6 // ... 20 other states
7
8 return (
9 <GlobalContext.Provider value={/* everything */}>
10 <Dashboard />
11 </GlobalContext.Provider>
12 );
13}
14
15// GOOD: State close to where it's used
16function App() {
17 // Only truly global data
18 const [user, setUser] = useState(null);
19 const [theme, setTheme] = useState('dark');
20
21 return (
22 <AppContext.Provider value={{ user, theme }}>
23 <Dashboard />
24 </AppContext.Provider>
25 );
26}
27
28function SearchPanel() {
29 // Local state - only this component needs it
30 const [searchQuery, setSearchQuery] = useState('');
31 return <input value={searchQuery} onChange={e => setSearchQuery(e.target.value)} />;
32}Rule of thumb: if state is used only by a single component or its direct children - keep it local. Move it up (lifting state up) or to context only when distant branches of the component tree need it.
Context API solves prop drilling without additional libraries. You learned about it earlier in this module. Its main strengths and limitations:
Strengths:
createContext, Provider, useContextLimitations:
1// Context API - good choice for ship configuration
2const ShipConfigContext = createContext();
3
4function ShipConfigProvider({ children }) {
5 const [config, setConfig] = useState({
6 theme: 'dark-nebula',
7 language: 'en',
8 soundEnabled: true,
9 });
10
11 return (
12 <ShipConfigContext.Provider value={{ config, setConfig }}>
13 {children}
14 </ShipConfigContext.Provider>
15 );
16}Redux is a proven state management pattern, inspired by the Flux architecture. It works like the central command center of a fleet - a single store holds the entire application state, and changes happen exclusively through dispatching actions. We write modern Redux with Redux Toolkit -
createSlice defines state and reducers, and configureStore builds the store:1// Redux Toolkit - the modern Redux standard
2import { createSlice, configureStore } from '@reduxjs/toolkit';
3
4// Slice = a piece of state + reducers + actions in one place
5const missionSlice = createSlice({
6 name: 'mission',
7 initialState: {
8 crew: [],
9 fuel: 100,
10 destination: null,
11 status: 'docked',
12 },
13 reducers: {
14 // Immer lets us "mutate" state - RTK turns it into a safe update
15 addCrewMember: (state, action) => {
16 state.crew.push(action.payload);
17 },
18 setDestination: (state, action) => {
19 state.destination = action.payload;
20 },
21 burnFuel: (state, action) => {
22 state.fuel = Math.max(0, state.fuel - action.payload);
23 },
24 launch: (state) => {
25 state.status = 'in-flight';
26 },
27 },
28});
29
30// createSlice automatically generates action creators
31export const { addCrewMember, setDestination, burnFuel, launch } = missionSlice.actions;
32
33// configureStore builds the store (with DevTools and middleware included)
34const store = configureStore({
35 reducer: {
36 mission: missionSlice.reducer,
37 },
38});
39
40// Usage in a component:
41function MissionControl() {
42 const dispatch = useDispatch();
43 const { crew, fuel, status } = useSelector(state => state.mission);
44
45 return (
46 <div>
47 <p>Status: {status} | Fuel: {fuel}% | Crew: {crew.length}</p>
48 <button onClick={() => dispatch(launch())}>Launch!</button>
49 <button onClick={() => dispatch(burnFuel(10))}>Burn fuel</button>
50 </div>
51 );
52}When Redux?
When NOT Redux?
Zustand is a minimalist state management library. If Redux is the main engine, Zustand is the agile maneuvering thruster - it does the same thing, but with less overhead. No providers, no boilerplate, no Context API re-render problems.
1// Zustand - simple store without providers
2// import { create } from 'zustand';
3
4// Creating a store - that's all you need!
5// const useShipStore = create((set) => ({
6// fuel: 100,
7// position: { x: 0, y: 0 },
8// crew: 5,
9//
10// burnFuel: (amount) => set((state) => ({
11// fuel: Math.max(0, state.fuel - amount)
12// })),
13//
14// moveTo: (x, y) => set({ position: { x, y } }),
15//
16// addCrew: () => set((state) => ({
17// crew: state.crew + 1
18// })),
19// }));
20
21// Usage in a component - no Provider needed!
22function FuelGauge() {
23 // Component re-renders ONLY when fuel changes
24 // const fuel = useShipStore((state) => state.fuel);
25 // return <div>Fuel: {fuel}%</div>;
26}
27
28function NavigationPanel() {
29 // Reads ONLY position - a fuel change doesn't cause a re-render
30 // const position = useShipStore((state) => state.position);
31 // const moveTo = useShipStore((state) => state.moveTo);
32 // return <button onClick={() => moveTo(10, 20)}>Fly!</button>;
33}Zustand automatically optimizes re-renders thanks to selectors. Each component subscribes only to the data it actually uses. A fuel change doesn't re-render the navigation panel - and vice versa.
| Feature | Context API | Redux Toolkit | Zustand | |---------|------------|---------------|---------| | Size | 0 KB (built-in) | ~11 KB | ~1.5 KB | | Boilerplate | Medium | Large (but RTK helps) | Minimal | | Re-renders | All consumers | Optimized (selectors) | Optimized (selectors) | | DevTools | No dedicated ones | Extensive (time-travel) | Redux DevTools extension | | Learning curve | Easy | Steep | Easy | | Best for | Theme, auth, locale | Large apps, large teams | Most applications |
At Jupiter Station every system has its purpose. The same goes for state management:
useState) - data needed only by a single component or its direct childrenRemember: there is no single best solution. The best space engineers match the tool to the problem, not the problem to the tool.
Test the pattern comparison in the editor below: