Blog
May 30, 2021 - 20 MIN READ
React as an Architecture Tool: Components, Hooks, and the Cost of Re-rendering

React as an Architecture Tool: Components, Hooks, and the Cost of Re-rendering

React is not a UI library — it’s a runtime for composing change. This post teaches the mental model that makes large React codebases predictable: component boundaries, hooks as architecture, and the real cost model of re-rendering (so you optimize the right thing).

Axel Domingues

Axel Domingues

In January I zoomed out: the web “compressed” itself into patterns that reduce latency and operational pain.

In February I zoomed in: HTTP is already a distributed systems API.

In March I hit browser reality: the event loop and rendering pipeline explain why UX bugs often smell like backend bugs.

In April we picked a data boundary — REST, GraphQL, tRPC — and admitted that the boundary is where coupling lives.

Now, May:

React is where all those decisions show up as user experience.

And here’s the uncomfortable truth:

In a real product, “frontend architecture” is less about pixels and more about change propagation.

React is a programming model for change propagation.

Treat it like architecture and it scales.
Treat it like “just UI” and your app becomes a haunted house of rerenders, stale state, and performance regressions no one can explain.

The goal this month

Build a mental model of React that survives a 50k+ LOC codebase and multiple teams.

The core idea

React architecture is where you place boundaries so change stays local and predictable.

The sharp edge

Re-rendering is cheap… until it isn’t. You need a cost model.

The payoff

Fewer “mystery bugs”, safer refactors, and performance work that actually moves user metrics.


React is a runtime, not a templating system

If you remember jQuery and “DOM as state,” React feels like a UI library.

If you operate large systems, React feels like this:

  • a scheduler (when work happens)
  • a pure computation phase (render)
  • a side-effect phase (commit + effects)
  • a diffing strategy (reconciliation)
  • a state propagation graph (who updates whom)

That’s architecture.

React in one sentence (the mental model I actually use):

A state change schedules a re-render of a component subtree; React computes “what UI should look like” (render), then commits minimal DOM updates and runs side effects.


The two-phase model: render vs commit

Most React confusion comes from mixing these phases.

Render phase

Pure computation: “given state + props, what should the UI be?”
No side effects. No DOM writes.

Commit phase

Apply changes to the DOM. Then run effects.
This is where you touch the outside world.

Why this matters

When a UI bug “sometimes happens,” ask:

  • Did I accidentally do a side effect during render?
  • Did I assume render happens once?
  • Did I rely on timing that isn’t guaranteed?

That’s the same mindset as backend distributed systems:

Don’t assume ordering you didn’t explicitly enforce.

If you want a single “architect’s rule” for React: render must be pure. If it touches the outside world, it belongs in the commit/effect world with explicit cleanup and idempotency.

Components are boundaries (not reusable HTML snippets)

Teams often treat components as “UI Lego.”

Architects treat components as module boundaries:

  • Where does state live?
  • Where do effects live?
  • Who is allowed to know about which dependencies?
  • Where do you enforce invariants?
  • What does “change” affect?

A simple boundary rule that scales

Push state down. Pull effects up.

  • “Down” gives locality (fewer rerenders, fewer hidden dependencies).
  • “Up” gives control (centralized orchestration, consistent error handling).

This is why container/presentational separation keeps reappearing — not because it’s fashionable, but because it keeps coupling visible.


Hooks are the architecture layer

Hooks aren’t “React magic.” Hooks are dependency injection for function components.

They let you encode “how this part of the system works” as reusable, testable logic.

The three classes of state (stop mixing them)

Most rerender pain comes from mixing these three kinds of state:

UI state

Local interaction: open/closed, selected tab, input focus, ephemeral values.

Server state

Remote truth with caching: loading, stale, refetch, pagination, invalidation.

App state

Cross-cutting session truth: auth, feature flags, locale, global preferences.

The rule

Different state types want different tools and different lifetimes.

If you put server state into a global store “because it’s shared,” you create:

  • cache invalidation bugs
  • inconsistent loading/error semantics
  • rerenders that ripple through the entire app

If you put app state into component state, you create:

  • duplicated truth
  • subtle drift across screens
  • navigation bugs

Hooks are where you encode the correct semantics for each class.


The cost model of re-rendering (what actually gets expensive)

People repeat “rerenders are cheap.” Sometimes they are.

But the full cost of a rerender can include:

  • running render functions (JS time)
  • allocating objects/arrays (GC pressure)
  • re-running expensive selectors or transforms
  • rendering large lists
  • creating unstable props that invalidate memoization
  • triggering child rerenders unintentionally
  • layout thrash when commits cause measurement

React’s diffing reduces DOM writes — but it cannot save you from expensive computation.

Think in budgets:

If you want 60fps, you have ~16ms per frame for everything — JS + layout + paint.

Your React work has to fit inside that budget where it matters (typing, scrolling, dragging, animations).

The four rerender traps I see most in production


Optimization without cargo cult: when to use memoization

React.memo, useMemo, and useCallback are not performance vitamins.

They are tradeoffs:

  • they add cognitive load
  • they can hide bugs (stale closures, wrong deps)
  • they can make performance worse if the memoization work exceeds the saved work
My rule: optimize only after you can answer:
  1. What is slow? (profile)
  2. Why is it slow? (root cause)
  3. What change reduces the cost? (measure again)

A practical “PR checklist” for rerender safety

Avoid accidental coupling

Are we passing “everything” down instead of defining a boundary?

Stabilize identity where it matters

Are we creating new objects/functions in hot paths?

Keep side effects out of render

Are effects centralized and idempotent?

Measure before “optimizing”

Did we use the React Profiler (or real metrics) before adding memoization?


Hooks that scale: patterns I actually trust

Pattern: Custom hooks as “domain adapters”

A good custom hook:

  • hides data fetching details
  • returns stable, UI-friendly outputs
  • encodes retries, cancellation, and error semantics
  • makes the component tree boring
type UseUserProfileResult = {
  user: { id: string; name: string } | null
  isLoading: boolean
  error: { message: string } | null
  refresh: () => void
}

function useUserProfile(userId: string): UseUserProfileResult {
  // placeholder: your real impl could use fetch, React Query, etc.
  // the point is the contract: UI doesn't care about transport details
  return { user: null, isLoading: true, error: null, refresh: () => {} }
}

This is the same move we do in backend architecture:

  • hide infrastructure details behind a stable interface
  • make the rest of the system depend on the contract, not the mechanism

Pattern: “effects are for synchronization, not computation”

If the value can be derived from state/props, derive it in render.

Effects are for syncing with the outside world:

  • network calls
  • subscriptions
  • timers
  • imperative libraries (maps, charts)
  • analytics
If your effect is “computing a value,” it’s often a smell.

You’re creating a second state machine that can drift.


A concrete example: why “loading spinners everywhere” is an architectural smell

If every component fetches its own data, you get:

  • waterfalls
  • duplicated loading states
  • duplicated error states
  • inconsistent retry semantics
  • rerenders that ripple unpredictably

The fix isn’t “better spinners.”

The fix is boundaries:

  • fetch at the boundary (route / feature container / BFF-aware boundary)
  • pass data down as props
  • keep widgets dumb

That’s why React architecture feels similar to microservices architecture:

It’s all about choosing where coordination lives.


Debugging React like a systems engineer

When something feels “random,” I walk this ladder:

Step 1 — Identify the trigger

What state update caused this?

  • local state?
  • context change?
  • server state refetch?
  • parent rerender?

Step 2 — Identify the boundary breach

Which component now knows something it shouldn’t?

  • a widget doing orchestration?
  • an effect hidden deep in the tree?
  • a “global store” carrying server state?

Step 3 — Identify the hot path

What re-renders frequently?

  • typing?
  • scroll?
  • animation?
  • websocket updates?

Step 4 — Profile and measure

Use the React Profiler (or production metrics if you have them).
Fix the biggest cost, not the most annoying code smell.

Step 5 — Install a guardrail

Make it hard to reintroduce the bug:

  • lint rules for hooks
  • patterns for stable props
  • feature boundaries
  • testable custom hooks

Closing: React is your UX CPU

React turns data changes into user experience.

If you treat it as an architecture tool:

  • your boundaries stay clean
  • your side effects stay predictable
  • your rerenders stay local
  • your performance work is rational

If you treat it as “just UI”:

  • your app becomes a global state soup
  • you’ll chase regressions with superstition
  • and your team velocity will collapse

May takeaway

React scales when you treat components as boundaries, hooks as architecture, and rerendering as a cost model (not a rumor).


Resources

React — Hooks Intro

The canonical foundation: why hooks exist and how to think in function components.

React — Using the Effect Hook

The semantics of effects, cleanup, and the mental model that prevents “random” UI bugs.

React Profiler (Intro)

If you optimize without profiling, you’re guessing. This turns guessing into diagnosis.

“A Complete Guide to useEffect”

A deep mental model for effects, dependencies, and stale closures.


FAQ


What’s Next

Now that React’s cost model is clear, we can talk about the “boring stack” that makes frontend systems scale:

  • routing
  • state boundaries
  • forms
  • validation
  • error and loading models
  • and how to keep feature work fast without inventing a framework every quarter
Axel Domingues - 2026