We use cookies to enhance your experience on the site
CodeWorlds

State management patterns

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.

The problem: Prop drilling

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.

State colocation - keep state close to where it's used

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 - the built-in solution

Context API solves prop drilling without additional libraries. You learned about it earlier in this module. Its main strengths and limitations:

Strengths:

  • Built into React - zero additional dependencies
  • Simple API:
    createContext
    ,
    Provider
    ,
    useContext
  • Excellent for data that changes rarely (theme, auth, locale)

Limitations:

  • Every value change re-renders ALL consumers
  • No built-in caching for API data
  • Requires combining with useReducer for complex state
  • Difficult performance optimization in large applications
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 - command center for large fleets

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?

  • Applications with many developers (clear rules, predictable data flow)
  • Complex state with many related actions
  • You need DevTools with time-travel debugging
  • Middleware for handling side-effects (thunks, sagas)

When NOT Redux?

  • Simple applications with few states
  • Small team or solo developer
  • Primarily API data (React Query is better)

Zustand - the lightweight maneuvering thruster

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.

Pattern comparison - galaxy map

| 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 |

The tool selection principle

At Jupiter Station every system has its purpose. The same goes for state management:

  1. Local state (
    useState
    )
    - data needed only by a single component or its direct children
  2. Lifted state - data shared by 2-3 nearby components, move it to their common parent
  3. Context API - global data that changes rarely: theme, auth, locale, language
  4. Zustand - global state in small/medium applications when you need performance and simplicity
  5. Redux Toolkit - large applications with many developers, complex business logic, need for DevTools

Remember: 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:

Go to CodeWorlds