
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
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.
If you remember jQuery and “DOM as state,” React feels like a UI library.
If you operate large systems, React feels like this:
That’s architecture.
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.
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.
When a UI bug “sometimes happens,” ask:
That’s the same mindset as backend distributed systems:
Don’t assume ordering you didn’t explicitly enforce.
Teams often treat components as “UI Lego.”
Architects treat components as module boundaries:
Push state down. Pull effects up.
This is why container/presentational separation keeps reappearing — not because it’s fashionable, but because it keeps coupling visible.
Boundary component (architecture):
Widget component (UI):
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.
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:
If you put app state into component state, you create:
Hooks are where you encode the correct semantics for each class.
People repeat “rerenders are cheap.” Sometimes they are.
But the full cost of a rerender can include:
React’s diffing reduces DOM writes — but it cannot save you from expensive computation.
Your React work has to fit inside that budget where it matters (typing, scrolling, dragging, animations).If you want 60fps, you have ~16ms per frame for everything — JS + layout + paint.
This is the classic.
// Every render creates a new object and function identity
<Child
options={{ dense: true }}
onSelect={(id) => setSelected(id)}
/>
Even if the values are “the same,” the identity changes, and memoization becomes useless.
Fix it when it matters with useMemo / useCallback — but only after measurement.
If you store derived data as state, you create drift.
const [name, setName] = useState(props.user.name) // drift risk
Prefer deriving during render, or store the minimal source of truth and derive from it.
Context is great for true global concerns.
Context is dangerous when you put frequently-changing values in it. One context update can rerender huge subtrees.
Patterns that help:
Large lists are where “cheap rerenders” go to die.
Fixes usually look like:
React.memo, useMemo, and useCallback are not performance vitamins.
They are tradeoffs:
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?
A good custom hook:
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:
If the value can be derived from state/props, derive it in render.
Effects are for syncing with the outside world:
You’re creating a second state machine that can drift.
If every component fetches its own data, you get:
The fix isn’t “better spinners.”
The fix is boundaries:
That’s why React architecture feels similar to microservices architecture:
It’s all about choosing where coordination lives.
When something feels “random,” I walk this ladder:
What state update caused this?
Which component now knows something it shouldn’t?
What re-renders frequently?
Use the React Profiler (or production metrics if you have them).
Fix the biggest cost, not the most annoying code smell.
Make it hard to reintroduce the bug:
React turns data changes into user experience.
If you treat it as an architecture tool:
If you treat it as “just UI”:
May takeaway
React scales when you treat components as boundaries, hooks as architecture, and rerendering as a cost model (not a rumor).
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.
Not automatically.
Rerendering is React’s mechanism for recomputing UI. It becomes a problem when rerenders:
Treat it like CPU: it’s fine until you exceed the budget.
No.
Memoization is a tradeoff:
Use it when profiling shows a real benefit — especially in hot paths.
Mixing state classes:
The second biggest mistake is putting orchestration logic deep inside widget components.
Keep side effects out of render, and treat effects as synchronization with the outside world.
If the value can be derived, derive it in render. If it touches the outside world, centralize and make it idempotent.
Now that React’s cost model is clear, we can talk about the “boring stack” that makes frontend systems scale:
Frontend Systems: Routing, State, Forms, and the “Boring Stack” That Scales
The month I stop treating “frontend architecture” as component trivia and start treating it as a system: URLs as contracts, state as truth, forms as transactions, and a boring stack you can operate.
AJAX → Fetch → GraphQL → tRPC: Choosing Your Data Boundary
Your data boundary is not “how we call the API.” It’s a coupling contract between teams, runtimes, and failure modes. This post gives you a practical decision framework for REST, GraphQL, and RPC-style boundaries (including tRPC) — with the tradeoffs that show up in production.