Advanced React & Rendering
Beyond components — reconciliation, the rules of hooks and why they exist, render performance, Server Components vs client, Suspense and concurrent rendering, and the state-colocation discipline that keeps large apps fast.
Advanced React & Rendering
In one line: Once you can build components, the expert step is understanding when and why React re-renders, what runs on the server vs the client, and how to keep a large app fast — because most React performance and bug problems are really "I didn't know what triggered this render."
This assumes you've done the React stage and shipped UI with useState/useEffect. It's the "competent → expert" layer: the mental model of rendering, the modern Server-Component split, and the performance discipline. You don't need it to build your first apps — reach for it when an app gets big enough to feel slow or tangled.
← New here? Start with the on-ramp: Frontend Frameworks.
Beginners think of React as "I write components and they show up." Experts think of it as "state changes, and React figures out the minimum DOM updates" — and they know exactly what causes that to happen. The single most useful upgrade is being able to answer "why did this component just re-render?" instantly: a parent re-rendered, its props/state changed, or its context value changed. Get that, plus where code runs (server vs browser) in modern React, and most performance mysteries and stale-data bugs evaporate.
- Reconciliation — React diffing the new render output against the previous one to compute minimal DOM changes.
- Render vs commit — render = call your components to produce a virtual tree; commit = apply the diff to the real DOM.
- RSC (React Server Components) — components that run only on the server and send serialized output, never shipping their JS to the browser.
- Hydration — attaching client-side interactivity to server-rendered HTML.
- Memoization — caching a value/component so it's not recomputed/re-rendered when inputs are unchanged.
- Concurrent rendering — React's ability to interrupt, pause, and resume rendering to keep the UI responsive.
The render model: why components re-run
A component re-renders when one of three things happens: its state changes, its parent re-renders, or a context it consumes changes. That's it. Crucially, "a parent re-rendered" cascades to all children by default — even children whose props didn't change. Re-rendering isn't the same as touching the DOM (reconciliation skips unchanged DOM), but the render functions still run, and that's where wasted work and subtle bugs live.
function Parent() {
const [count, setCount] = useState(0);
// Every click re-renders Parent AND <ExpensiveList/> — even though the list's
// data never changed — because Parent re-rendered. That's the default.
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExpensiveList items={items} />
</>
);
}
The fix is rarely "wrap everything in memo." It's usually state colocation (push state down to the smallest component that needs it) and lifting content up via children so a frequently-updating component doesn't own a subtree it doesn't affect.
Try it live — open the console (in the preview) and click the button. The parent's state change re-renders Parent and Child even though Child's props never change — the default cascade. Edit the code to wrap Child in React.memo and watch the extra render disappear:
The rules of hooks (and the why)
Hooks must be called unconditionally, in the same order, every render — no hooks in conditions, loops, or after an early return. The reason is mechanical: React tracks hook state by call order, not by name. Call them in a different order and useState #2 returns hook #1's value — silent, vicious bugs. The lint rule react-hooks/exhaustive-deps exists for the same class of problem: a useEffect whose dependency array omits a value it reads will run with stale closures. Understanding why (closures capture the render they were created in) is what separates "I fight useEffect" from "I reach for it deliberately."
The biggest React-maturity tell is not reaching for useEffect. You do not need an effect to: transform data for rendering (do it during render), respond to a user event (do it in the handler), or reset state on prop change (use a key). Effects are for synchronizing with external systems — a subscription, a non-React widget, the document title. "I'll just add a useEffect to keep these two states in sync" is the #1 source of render loops, double-fetches, and flicker. If you find an effect that only adjusts other React state, delete it.
Server Components vs client: where code runs
Modern React (App Router / RSC) splits components by where they run:
| Server Component (default) | Client Component ("use client") | |
|---|---|---|
| Runs | On the server, once per request | In the browser (and server, for SSR) |
| Can | await data directly, read secrets, hit the DB | Use state, effects, event handlers, browser APIs |
| Ships JS to browser? | No — only its output | Yes — its code is in the bundle |
| Use for | Data fetching, static content, large deps | Interactivity, anything stateful |
The expert discipline: keep components as Server Components by default, and push "use client" to the leaves (the actual interactive bits). A common mistake is marking a big top-level layout "use client", which drags its entire subtree into the client bundle. The mental model: server components are for getting and shaping data; client components are for reacting to the user.
Almost every React performance problem reduces to unnecessary renders, and almost every "stale value" bug reduces to a closure capturing an old render. So the senior habit is to always be able to answer two questions: (1) what state change triggered this render, and does this component actually depend on it? and (2) which render did this closure capture? With React DevTools' "Highlight updates" and the Profiler, you can see renders happening; with that visibility, memo/useMemo/useCallback stop being cargo-culted everywhere and become targeted fixes for a measured problem. Optimize what the profiler shows you, not what you guess.
Performance levers (in priority order)
- State colocation — the cheapest, biggest win. State that lives lower re-renders less.
children/ composition — pass expensive subtrees aschildrenso a stateful wrapper doesn't re-render them.- Keys — stable, meaningful
keys (never array index for dynamic lists) so reconciliation reuses DOM instead of recreating it. memo/useMemo/useCallback— only on a proven-hot path. Each has its own cost; sprinkling them everywhere can be net-negative. As of mid-2026, the React Compiler (1.0 GA, opt-in) auto-inserts this memoization for you from plain code — so on a compiler-enabled project, hand-writtenuseMemo/useCallbackshould be the rare exception the profiler asks for, not the default reflex.- Virtualization — for long lists, render only what's visible (TanStack Virtual).
- Suspense + streaming — show meaningful UI while data loads, stream from the server.
Common mistakes
- Reaching for
useEffectto derive or sync state. Derive during render; handle events in handlers; reset with akey. Effects are for external systems only. memo-ing everything. Memoization has overhead and is usually defeated by a new object/function prop each render anyway. Profile first; memoize the hot path.- Array index as
keyfor dynamic lists. Causes wrong state/DOM reuse on reorder or deletion. Use a stable id. - Marking a high-level component
"use client". It pulls the whole subtree into the bundle. Keep server-by-default; push"use client"to the interactive leaves. - Fetching in
useEffectwhen the framework offers server fetching. Server Components / loaders fetch on the server with no waterfall and no client bundle cost. - Ignoring the dependency array warning. A wrong deps array is a stale-closure bug waiting to happen. Fix the deps or restructure; don't suppress the lint.
Practice on your own project
- Open React DevTools → "Highlight updates," click around, and find a component re-rendering more than it should — then explain why (parent re-rendered? state? context?).
- Find a
useEffectand decide whether it's legitimate (syncing with an external system) or should be deleted (deriving state / responding to an event). Remove at least one. - Pick one piece of state that's lifted higher than it needs to be and colocate it lower.
- If you're on the Next.js App Router, find a component marked
"use client"that could be a Server Component and push the boundary down toward the leaves.
Page checkpoint
Did advanced React stick?
RequiredWhat's next
→ Continue to Styling — and its advanced companion — for the layer that turns a working component tree into a polished UI.