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



This content originally appeared on DEV Community and was authored by Riturathin Sharma

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>

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

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

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: {}
  }
}

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>

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>

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”

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.


This content originally appeared on DEV Community and was authored by Riturathin Sharma