DEV Community

Riturathin Sharma
Riturathin Sharma

Posted on

Rethinking State Management in React: A UI Architect’s Deep Dive Into “State Boundaries”

In the React ecosystem, we’ve spent years debating which state management library to use — Redux, MobX, Recoil, Zustand, Jotai, XState, and the ever-humble React Context.

But the real architectural question we often forget is far more fundamental:

  • Where should state live?
  • How far should it travel?
  • And how do we prevent state from becoming a silent source of UI slowdown?

Let's discuss in depth on an unglamorous yet powerful topic that every UI architect should obsess about:

State Boundaries — The Most Important Concept in Scalable React Apps

State boundaries define how far a piece of state is allowed to influence the UI.

If your components re-render “too much,” lag under load, or behave unpredictably, the root cause 90% of the time is:

Your state boundaries are broken.

Let’s understand this architecturally.

1. Why State Boundaries Matter More Than the Library You Choose

Most engineers think state problems are library problems:

“Redux is too verbose”

“Context causes re-renders”

“Zustand feels easier”

“useState is enough for everything”

“React Query replaces global state”

…but performance issues rarely stem from the library. They stem from incorrect scoping.

Poor state boundaries → excess re-renders → wasted CPU → janky UI.

A simple example:

// ❌ Anti-pattern
<App>
  <Navbar user={user} />
  <Sidebar user={user} />
  <Dashboard user={user} />
</App>
Enter fullscreen mode Exit fullscreen mode

You’ve just created a gigantic state boundary — one tiny state change in user will re-render half the application.

Now imagine 200 components relying on the same global state.
This is how “React feels slow” stories are born.

2. The Architect’s Rule: “State Should Be as Local as Possible”

React is built on the idea of locality of state.

A simple rule:

Move state down until it stops mattering to everyone above it.
Move shared state up only when two siblings need it.

Micro Example:

A toggle button controlling its own UI.

function Toggle() {
  const [on, setOn] = useState(false);
  return <button onClick={() => setOn(!on)}>{on ? "ON" : "OFF"}</button>
}
Enter fullscreen mode Exit fullscreen mode

Perfectly local. No need for global state. No need for context. Zero re-renders outside the component.

Macro Example:

A user object needed by 15 components across the page.

Context or Zustand makes sense here.

But what if only two components need the user’s theme preference?
Then create a smaller local boundary:

User State (global)
├── Theme Preference (local to UI shell)
└── User Metadata (local to specific feature)

Architectures break when everything is global “just in case.”

3. The Hidden Performance Killer: “Derived State Leaks”

A subtle but deadly anti-pattern:

const user = useUserStore(state => state.user);

const isAdmin = user.role === 'admin';  `// ❌ derived but inside` global boundary

Enter fullscreen mode Exit fullscreen mode

This component now re-renders whenever anything inside user changes — phone number, profile pic, status, anything.

Fix: keep derived state as close to its usage as possible.

const isAdmin = useUserStore(state => state.user.role === 'admin'); // ✔ only re-renders when role changes

You've just reduced re-renders by 90%.

4. State Machines: Creating Predictable Boundaries

One of the cleanest ways to define strict state boundaries is using UI state machines (XState or custom).

Example: A checkout UI.

const checkoutMachine = {
  id: "checkout",
  initial: "cart",
  states: {
    cart: { on: { NEXT: "address" }},
    address: { on: { NEXT: "payment", BACK: "cart" }},
    payment: { on: { NEXT: "review", BACK: "address" }},
    review: { on: { SUBMIT: "complete", BACK: "payment" }},
    complete: {}
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, each boundary is a strict, predictable “mode.”

Why is this beneficial?

- UI cannot accidentally access out-of-scope state
- Reduces invalid transitions
- Less conditional UI logic (“if cart then… else if payment then…”)
- Fewer re-renders since each state is independent

5. How to Design State Boundaries in a Real Project (Architect’s Checklist)

Step 1:
Identify State Types

  1. Local UI state (modals, text inputs)
  2. Server state (React Query / SWR)
  3. Global app state (theme, auth, settings)
  4. Ephemeral UI logic (hover, focus, scroll)

✔ Step 2: Assign State Ownership

  1. Local state → useState, useReducer
  2. Shared UI state → Context (carefully)
  3. Large-scale global logic → Zustand / Redux
  4. Data fetching → React Query
  5. Flow control → state machines

✔ Step 3: Prevent State Leakage

  1. Map state slices with selectors
  2. Avoid passing entire objects down props
  3. Memoize expensive selectors
  4. Use “compound component patterns” for local boundaries

✔ Step 4: Measure before optimizing

Use:

  1. React DevTools Profiler
  2. Flamegraph
  3. Why Did You Render (WDR)
  4. Browser performance panel

If a boundary causes 30+ rerenders, consider splitting it.

  1. Example: A Poorly Architected Component Tree
<App>
  <UserProvider>          // global and huge
    <Layout>
      <Sidebar />
      <Dashboard />
      <Notifications />
      <Search />
      <Profile />
    </Layout>
  </UserProvider>
</App>
Enter fullscreen mode Exit fullscreen mode

If user changes → everything re-renders.

  1. Same App With Better State Boundaries
<App>
  <AuthProvider> // only login/logout
    <Layout>
      <UserPreferencesProvider> // theme, language
        <Sidebar />
        <Profile />
      </UserPreferencesProvider>

      <Dashboard /> // fetches its own data

      <NotificationProvider>
        <Notifications />
      </NotificationProvider>

      <Search /> // local debounced state
    </Layout>
  </AuthProvider>
</App>

Enter fullscreen mode Exit fullscreen mode

Each provider now has:

  • isolated re-renders
  • purpose-specific state
  • predictable ownership
  • This is real UI architecture.
  1. The Takeaway (And What Most Engineers Miss)
React doesn’t get slow because:
“React is slow”
“Context is bad”
“Redux is heavy”

Enter fullscreen mode Exit fullscreen mode

React gets slow because state boundaries are drawn carelessly.

A UI Architect’s job isn’t to decide between Redux or Zustand.
It is to decide where state should live and where it shouldn’t.

When boundaries are clean:

Features become independent

Re-renders shrink

Debugging becomes trivial

Onboarding becomes easier

Performance becomes natural—not an afterthought

Final Thoughts

In a world obsessed with new React libraries, one of the most traditional—and most overlooked—topics remains the true pillar of scalable UI architecture:

State Boundaries decide whether your React app survives scale or collapses under it.

If you master this, tools become replaceable.
Architecture becomes future-proof.
Teams become faster.

And your React UI becomes predictable, fast, and delightful.

Top comments (0)