Modular State Machines with Redux as Orchestration Layer

Here’s the same article formatted specifically for Medium, where Markdown is supported but with some quirks. I’ve used headers, code blocks, and spacing that render cleanly on Medium.

You can copy-paste this directly into Medium’s editor:

Modular State Machines with Redux as Orchestration Layer

In our front-end architecture, we follow a simple but powerful rule:

If state doesn’t need to be global, it stays local in a state machine. If it does, it lives in Redux.

This article outlines how we combine modular, encapsulated components with centralized coordination using Redux—without sacrificing clarity, performance, or maintainability.

🧱 The Architecture in a Nutshell

Each component in our system is a fully enclosed unit:

  • Internal logic is managed by a state machine via a custom useMachine() hook.
  • Internal communication (across subcomponents) uses React Context.
  • Redux is used solely as an external coordination layer—for global awareness and cross-component collaboration.
Redux (initial state)
   ↓
Component (hydrate from Redux)
   ↓
Internal State Machine (owns transitions)
   ↓
Redux (re-broadcasts if other components need to know)

Why This Works

✅ Local Machines = Encapsulation

Each component:

  • Owns its state and transitions.
  • Remains testable and independent.
  • Avoids accidental tight coupling with global state logic.

Example: A PoolSelector manages user selections, transitions, and validations entirely within its own state machine. This makes it portable, reusable, and easier to debug.

✅ Redux = Shared Awareness

Redux still plays an essential role, but only for:

  • Hydrating components with saved views (e.g. filter presets, route state).
  • Broadcasting changes that other components may care about.
  • Time travel/debugging/devtools.

This keeps Redux clean and focused on coordination, not micromanagement.

The Lifecycle of State

  1. Hydration
    On mount, components hydrate their internal machines from Redux.
const filters = useSelector(selectSavedFilters)

useEffect(() => {
  send({ type: 'HYDRATE', filters })
}, [])
  1. Ownership
    After hydration, the state machine drives the UX independently:
  • Transient UI states
  • Validation logic
  • User interactions
  1. Broadcasting
    On meaningful state changes (e.g. final submission), components push updates back into Redux.
if (state.matches('submitted')) {
  dispatch(setFilters({ filters: ctx.filters }))
}

What About State Duplication?

Yes, there’s intentional duplication:

  • Redux holds a simplified snapshot (e.g. selected pool ID, filter summary).
  • The state machine holds richer context (e.g. step logic, validation flags, pending status).

This is not a flaw—it’s a feature.

🔑 Duplication is worth it when it provides clarity and decoupling.

Rather than deeply coupling internal UI state to Redux, we:

  • Keep the source of truth local, post-hydration.
  • Share only the minimal metadata other components need.

Controlling from Outside

If external actors (like other components or global actions) need to influence a component, we use:

  • Redux actions as commands.
  • Internal components observe those commands and act accordingly.
const shouldReset = useSelector(selectShouldReset)

useEffect(() => {
  if (shouldReset) send('RESET')
}, [shouldReset])

This lets components remain decoupled while still being orchestratable.

Best Practices

  • ✅ Hydrate once, don’t continuously sync from Redux.
  • ✅ Broadcast selectively—on state boundaries, not every keystroke.
  • ✅ Document state ownership: who owns it, who reads it, who can write.
  • ✅ Use Redux page slices (e.g. per view) to avoid global namespace collisions.
  • ⚠️ Avoid cyclical flows (Redux triggers machine, machine updates Redux, repeat).

Summary

This pattern gives us:

  • ✅ Modular, testable, storybook-ready components
  • ✅ Global awareness without tight coupling
  • ✅ Clear state ownership and lifecycle control

We’ve found it scales beautifully across dashboards, forms, interactive widgets, and multi-step workflows.

If you’re using Redux, XState, or any finite state machine pattern—consider using Redux as your global observer, not your micromanager.

Have thoughts or questions?
I’d love to hear how you structure state in your frontends—especially in large-scale or multi-team codebases.

Similar Posts