Part 13 · 4 chapters · ~30 min

React On The Wire

Hydration is a render that claims DOM nodes instead of creating them, and a mismatch is two renders that saw different inputs. This part is hydration node by node with every mismatch source and its fix, the architectures that reduce hydration (islands, partial, Server Components, resumability) and how to choose, React in a WebView and in React Native, and the course in one page.

64

Hydration: what happens, and what goes wrong

the question

"Hydration mismatch: the server rendered one thing and the client another. The page looks fine. Why is it an error, and what is React doing?"

Hydration is a render that claims existing DOM nodes instead of creating them, checking as it goes that each node is what the client would have produced. A mismatch means the client's first render disagreed with the server's for the same inputs, which means one of them was reading something the other could not (time, the window, storage, randomness) or the HTML was invalid. React recovers (patching text, or re-rendering a subtree from scratch), but the recovery costs the SSR work and may flash. The fix is always to make both renders see the same inputs, or to move the differing part after mount.

code
// the five mismatch shapes and their fixes
// 1. time
function Updated({ at }) { return <p>{formatRelative(at, Date.now())}</p> }                 // server: "5 minutes ago"; client, later: "6 minutes ago"
function Updated({ at }) { const now = useNow(); return <p>{formatRelative(at, now)}</p> }    // useNow: useSyncExternalStore with getServerSnapshot = () => serverNow (passed from the server), updates after mount
// or: render the absolute time on both, and upgrade to relative in an effect; or suppressHydrationWarning for a timestamp you accept differing

// 2. environment
function Nav() { const mobile = window.innerWidth < 600 }                                    // throws on the server; or mismatches if guarded with typeof window
function Nav() { const mobile = useMediaQuery('(max-width: 600px)', { ssr: false }) }         // returns a server value (false) on both until mount, then the real one
// pattern: const isClient = useSyncExternalStore(() => () => {}, () => true, () => false)   // false on the server and during hydration; true after

// 3. storage and auth
function Theme() { const t = localStorage.getItem('theme') }                                 // server has no localStorage; client's first render differs
// fix: put the theme in a cookie (the server reads it); or a server snapshot of "light" and switch after mount (a brief flash, or a blocking inline script that sets a class before paint)

// 4. randomness and ids
<label htmlFor={`f-${Math.random()}`}>                                                      // different on both
<label htmlFor={useId()}>                                                                    // stable from the tree position; identical on server and client

// 5. invalid nesting
<p><div>…</div></p>                                                                          // the browser closes the <p> before the <div>: the DOM tree differs from the JSX
// fix: valid HTML (the browser course part 2 has the tree builder rules; React 19 errors on known invalid nestings in dev)
the mechanics
  1. hydrateRoot sets a hydration flag; beginWork for host fibers calls tryToClaimNextHydratableInstance, which walks the DOM in document order alongside the fiber walk, claiming elements and text nodes. Attributes are not re-applied in production (trusted); development compares.
  2. Events need no per-node attachment: the root listener dispatches by fiber. Hydrating a subtree makes its handlers live.
  3. Commit has no mutations for claimed nodes; refs attach in layout; effects run as on any mount.
  4. Text mismatch: patched in production, reported in development. Structural mismatch: the nearest Suspense boundary's subtree is discarded and client-rendered (onRecoverableError reports it); without a boundary, the root is.
  5. Suspense boundaries during hydration: dehydrated boundaries (content not yet streamed, or lazy code not yet loaded) are skipped and hydrated later; a boundary whose content changed on the client (data differs) falls back to client rendering for that boundary only.
the discipline
  1. Render from data the server also had. Props from the server, a cookie for preferences, a server snapshot for environment values.
  2. Defer the rest to after mount: an effect, or useSyncExternalStore with a server snapshot that differs from the client snapshot (React handles the switch without a mismatch).
  3. Use useId for ids; never random values in render.
  4. Write valid HTML; React 19 errors on known-invalid nesting in development.
  5. Suspense boundaries around regions so a recovery re-renders a card, not the page.
  6. Treat suppressHydrationWarning as an annotation for one intentional text difference, not a fix.
HYDRATION, NODE BY NODE
what React does with server HTML, and where mismatches come from
swipe the figure sideways, or tap expand for full screen
1/6
server HTML
The server HTML:

Hello

Updated 5 minutes ago

. The client bundle loads; hydrateRoot(root, ) begins: a render with isHydrating = true.
65

Islands, partial hydration, and the alternatives

The cost of hydration is proportional to the components that must run on the client to become interactive. Every architecture that reduces it does so by shipping less or running less: islands ship code for marked components only; partial hydration spreads and prioritises one tree's hydration; Server Components remove server-only code from the bundle entirely; resumability skips running components on load altogether. They are answers to different page shapes.

the models
  1. Full hydration: the baseline. HTML from the server; the whole bundle; the whole tree runs on the client. Interactive when everything is. Right for small apps and for pages that are mostly interactive anyway.
  2. Islands (Astro, Fresh, Marko, Eleventy): static HTML; independently hydrated components with triggers (load, idle, visible, media). Zero JS for content; no shared React state between islands. Right for content sites with isolated interactive widgets.
  3. Partial and progressive hydration (React 18 Suspense SSR, part 5): one tree, streamed; boundaries hydrated lazily and by interaction priority. The bundle still contains the tree; the work is spread. Right for applications with a heavy but coherent UI.
  4. Server Components with client boundaries (part 6): server parts ship elements, not code; client parts hydrate. Islands within one tree, with state via props and children. The React answer; the default in Next.js App Router.
  5. Resumability (Qwik): state and handler locations serialised into HTML; no component runs until an event needs it. Zero hydration; a different framework with serialisability constraints.
what to measure
  1. Time to interactive for the thing the user will touch first, on the slowest device you support. Not total JS; not Lighthouse's generic TTI; the specific interaction (the browser course part 12's INP, measured for the first interaction).
  2. JS shipped per route, split by server-only versus client; the bundle analyser with the RSC boundary visible.
  3. Hydration time per boundary in the React Profiler; boundaries that take long and are never interacted with are candidates for lazy triggers or for becoming server-only.
ISLANDS, PARTIAL, PROGRESSIVE, RESUMABLE
four answers to "how much JavaScript does a page need"
swipe the figure sideways, or tap expand for full screen
1/6
full hydration
Full hydration (classic SSR): the server renders HTML; the client downloads the whole bundle, runs every component to rebuild the fiber tree, and attaches handlers. Cost: the full bundle and a render of everything before anything is interactive. Interactive-after-hydration is the metric; a large page on a slow phone can be seconds of visible-but-dead UI.
66

Hybrid apps, WebViews, and React Native

code
// React inside a WebView, or a native shell around a React web app
// 1. the bridge: window.ReactNativeWebView.postMessage(json) → native; native → webView.evaluateJavaScript / injectJavaScript → window.onNativeMessage
//    treat it as a network boundary: typed messages, versioned, with acks; never assume synchronous replies
// 2. what the web app cannot do natively well: file pickers, camera, biometrics, push, deep links, the back button, safe areas, keyboard avoidance
//    → native handles these and the bridge carries results; the React app renders state it receives
// 3. navigation: one of (a) the web app owns routing inside the WebView (history API; native back button → window.history.back() via the bridge),
//    or (b) native owns screens and each WebView is one page (no SPA navigation; cheaper mental model; more WebView instances)
// 4. performance: a WebView is a browser; everything in the browser course applies, plus: cold start of the WebView itself (~300 to 800 ms on Android),
//    no shared cache with the system browser, and an older engine on some devices (Android System WebView updates lag). ship for the oldest you support
// 5. auth: cookies in a WebView are per-app; tokens via the bridge or a cookie set by native. do not rely on the system browser's session
// 6. hydration in a WebView: same rules; the first render often comes from a bundled HTML file (file:// or an app scheme), so SSR may be a build-time render
// 7. React Native (not a WebView): the same React, a different renderer (Fabric) producing native views; no DOM, no CSS, the same fiber, hooks, scheduler, reconciliation.
//    parts 1 to 5 of this course apply unchanged; parts 9, 12, 13 differ in the host
React in a WebView
  1. It is a browser with everything the browser course describes, plus a cold start of its own (hundreds of milliseconds on Android), no shared cache with the system browser, cookies scoped to the app, and an engine version you do not control (Android System WebView lags; iOS WKWebView tracks Safari). Ship for the oldest engine you support; measure on it.
  2. The bridge is a network: asynchronous, typed, versioned messages with acknowledgements; the React app renders state it receives and sends intents it cannot fulfil (camera, biometrics, push, payments, deep links, safe areas, the hardware back button).
  3. Navigation ownership: either the web app routes inside one WebView (and the native back button is forwarded to history.back()), or native owns screens with a WebView per page. Mixing them is where back-button bugs live.
  4. Hydration from a bundled HTML (an app-scheme or file URL) is a build-time SSR; the same mismatch rules apply, with the environment differences (no cookies at build time) more likely.
  5. Auth and session: tokens via the bridge or cookies set natively; never assume the system browser's session.
React Native
  1. The same React core: fiber, hooks, the scheduler, reconciliation, lanes, Suspense (parts 1 to 5 apply unchanged). A different renderer (Fabric) produces native views instead of DOM nodes; the host components are View, Text, Image; styles are a subset of CSS semantics via StyleSheet; layout is Yoga (a flexbox implementation; the algorithms course part 3's flex algorithm).
  2. What differs: no DOM (no refs to elements in the web sense; measure via onLayout), the JavaScript thread versus the UI thread (animations on the UI thread via Reanimated; gestures via the gesture handler), the bridge or JSI for native modules, and startup (Hermes precompiles bytecode for exactly the startup reasons in the JS course part 1).
  3. Shared code: hooks, state, data fetching and business logic are the same; components diverge at the host layer. React Native Web and Expo's web target let one tree render both, with the platform differences at the edges.
67

The course, in one page

the internals
  1. Three trees: elements you create, fibers React keeps, DOM the browser owns; render is a function call React makes when it likes. (P0)
  2. Fiber: thirty fields, two trees, one pointer; beginWork renders, completeWork builds detached host subtrees, commit applies in three passes then paints then runs passive effects. (P1)
  3. Reconciliation: type and key decide identity; keys make arrays O(n); bailouts are reference checks; context propagation walks the subtree. (P2)
  4. Lanes: priority from the event; bits in a Smi; sync renders uninterruptibly, transitions yield and restart, expiration prevents starvation. (P3)
  5. Hooks: a positional list; a queue per state hook processed by lane with a base queue; effects as tagged objects run in their commit pass. (P4)
  6. Concurrent: transitions, Suspense as thrown promises, streaming with per-boundary and selective hydration, tearing and uSES. (P5)
  7. Server: components that send their output not their code; the Flight payload; actions as endpoints; the compiler memoising from an SSA analysis. (P6)
the practice
  1. State sorted by owner: local, shared, server, URL, form, session; context for injection; stores for hot shared state; caches for server state. (P7)
  2. Data: start requests at the route, read in components, cache by key, invalidate after mutations, optimistic for the reversible. (P8)
  3. Forms: uncontrolled by default; validate on blur and submit and server; actions for pending and progressive enhancement; integer money and idempotency keys. (P9)
  4. Patterns: composition over configuration; compound parts over a scoped context; headless behaviour; render props for controlled positions; state colocated. (P10)
  5. Anti-patterns: effect chains, derived state in state, context as a store, inner components, unstable props, premature memo; each a Profiler signature. (P11)
  6. Testing: component tests by role and user event; MSW at the wire; findBy for async; few e2e on money paths; stories for pixels. (P12)
  7. The wire: hydration claims nodes and recovers from mismatches; islands, partial, RSC and resumability trade JS for interactivity differently; a WebView is a browser with a bridge; React Native is the same core with a different host. (P13)
what to do next
Open the React DevTools Profiler on the slowest interaction in something you ship, with "record why each component rendered" on. Every row in part 11's table is a reason you will see there, and every fix points back to a mechanism in parts 1 to 6.