Hydration & Islands Architecture
โฑ๏ธ ~3-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.โ Instant coffee vs fresh brew โ SSR HTML is ready immediately (visible) but flat (no interactivity). Hydration adds hot water (JavaScript) to bring it to life.
What hydration actually does
SSR generates HTML on the server. The browser renders it immediately โ users see content fast. But static HTML has no event handlers, no React state, no refs. Hydration is the process of React "attaching" to existing DOM nodes: walking the server-rendered HTML, matching it to the virtual DOM, and setting up event listeners without re-rendering.
| 1 | // Server: render to HTML |
| 2 | const html = ReactDOMServer.renderToString(<App />); |
| 3 | // Sends: <button data-reactroot="">Click me</button> |
| 4 | ย |
| 5 | // Client: attach React without re-rendering |
| 6 | ReactDOM.hydrateRoot(document.getElementById('root'), <App />); |
| 7 | // React walks the DOM, matches it to <App />'s virtual DOM, |
| 8 | // attaches onClick handlers, creates state, sets up refs. |
| 9 | // If server HTML === client HTML: no DOM mutations needed. |
| 10 | // If mismatch: React logs a warning and re-renders from scratch. |
The hydration mismatch problem
Hydration mismatches occur when the HTML the server generated doesn't match the HTML React would generate on the client. React will log a warning and re-render the affected subtree from scratch โ breaking the "no re-render needed" optimization.
Date.now()
Fix: Use a static date in SSR, or suppress with suppressHydrationWarning
Math.random()
Fix: Generate random values server-side only, pass as props
window/document access in render
Fix: Guard with typeof window !== 'undefined' or useEffect
Browser-only APIs (localStorage)
Fix: Read in useEffect โ never during render
CSS-in-JS without SSR support
Fix: Use SSR-compatible styled-components or use CSS modules
Astro islands โ client directives
| 1 | --- |
| 2 | // Astro component (server-rendered by default) |
| 3 | const posts = await db.query('SELECT * FROM posts'); |
| 4 | --- |
| 5 | <html> |
| 6 | <!-- Static โ zero JS, just HTML --> |
| 7 | <PostList posts={posts} /> |
| 8 | ย |
| 9 | <!-- Island: hydrate immediately when JS loads --> |
| 10 | <SearchBar client:load /> |
| 11 | ย |
| 12 | <!-- Island: hydrate only when component is visible --> |
| 13 | <CommentSection client:visible /> |
| 14 | ย |
| 15 | <!-- Island: hydrate when browser is idle --> |
| 16 | <Analytics client:idle /> |
| 17 | ย |
| 18 | <!-- Island: hydrate only on media query match --> |
| 19 | <MobileMenu client:media="(max-width: 768px)" /> |
| 20 | </html> |
Interactive Sandbox
โ Move something, see it react instantly.Hydration strategy
JS size (initial)
800KB
Time to Interactive
2.2s
Traditional React SSR/SSG. Server generates full HTML. Browser downloads the entire JS bundle, parses it, then hydrates the entire component tree before any interaction is possible.
Hydration timeline (from navigation)
Challenge
Explore all 5 hydration patterns. Track the JS size and TTI for each. Which pattern achieves the fastest TTI? What's the trade-off that makes it possible?
Why Should I Care?
โ The exact interview question + the bug it kills.Interview questions
Q: What is the "hydration mismatch" error and why does it occur?
React expects the client-rendered HTML to exactly match the server-rendered HTML. If they differ, React logs "Warning: Text content did not match" and falls back to a full client-side re-render โ losing the SSR performance benefit. Common causes: using Date.now() or Math.random() in render (different values server vs client), reading browser APIs like localStorage or window.innerWidth during render, or rendering based on cookies that aren't available on the server.
Q: How does Astro's islands differ from React's selective hydration?
React's selective hydration (React 18) is within a single React tree โ it prioritizes hydrating components the user is interacting with. All components share state and context. Astro's islands are completely separate React roots โ each island is an independent application. Islands cannot share React context, so communication requires non-React mechanisms (custom events, URL params). Astro achieves smaller JS because most of the page doesn't ship any JS at all.
Bug: Rendering different content server vs client
| 1 | // โ Date.now() produces different values โ mismatch |
| 2 | function Timestamp() { |
| 3 | return <time>{new Date(Date.now()).toLocaleString()}</time>; |
| 4 | } |
| 5 | ย |
| 6 | // โ Fix 1: Read in useEffect (client-only) |
| 7 | function Timestamp() { |
| 8 | const [time, setTime] = useState(''); |
| 9 | useEffect(() => { setTime(new Date().toLocaleString()); }, []); |
| 10 | return <time suppressHydrationWarning>{time}</time>; |
| 11 | } |
| 12 | ย |
| 13 | // โ Fix 2: Pass static value as prop from server |
| 14 | // Server: <Timestamp initial={new Date().toISOString()} /> |
| 15 | function Timestamp({ initial }: { initial: string }) { |
| 16 | return <time>{new Date(initial).toLocaleString()}</time>; |
| 17 | } |
The Deep Dive
โ Spec refs, engine internals, the minutiae.React 18 selective hydration (hydrateRoot)
| 1 | // React 18: selective hydration with Suspense |
| 2 | // Server: streams HTML with Suspense boundaries |
| 3 | <html> |
| 4 | <Header /> {/* streams immediately */} |
| 5 | <Suspense fallback={<Spinner />}> |
| 6 | <ArticleBody /> {/* streams when ready */} |
| 7 | </Suspense> |
| 8 | <Suspense fallback={<Spinner />}> |
| 9 | <Comments /> {/* independent stream */} |
| 10 | </Suspense> |
| 11 | </html> |
| 12 | ย |
| 13 | // Client: hydrateRoot |
| 14 | hydrateRoot(document.getElementById('root'), <App />); |
| 15 | // React hydrates in priority order: |
| 16 | // 1. Components the user is interacting with (clicking) |
| 17 | // 2. Above-fold components |
| 18 | // 3. Below-fold components |
| 19 | // User can interact with Header while Comments still hydrating |
Qwik resumability internals
Qwik serializes component state and event handler function references directly into HTML attributes. The Qwik loader (~1KB) adds a global click listener. On the first interaction, it reads the serialized handler reference from the HTML, lazily downloads the relevant component code, deserializes state, and executes. No component code runs until the user actually interacts with it.
| 1 | <!-- Qwik serializes handler references into HTML --> |
| 2 | <button on:click="/src/routes/index_component_div_button_onClick_qhkJn9TsCgI.js#onClick"> |
| 3 | Add to Cart |
| 4 | </button> |
| 5 | <!-- Qwik loader sees this click, downloads the .js chunk, |
| 6 | executes onClick โ no React hydration ever happened --> |
RSC wire format โ not HTML, not JSON
React Server Components stream a special wire format (sometimes called the "RSC payload") โ not HTML and not JSON. It's a line-delimited format where each line is a serialized React element tree, including client component boundaries and their props. This allows streaming incremental UI updates without full page navigation, and enables the client to build the React component tree without re-running server component logic.
Interview Questions
โ Real questions from real interviews โ with answers.Hydration is React attaching event listeners and internal state to server-rendered HTML without re-creating DOM nodes.
Mismatch occurs when server and client render different HTML โ caused by Date.now(), Math.random(), window access, or localStorage reads during render.
Partial hydration hydrates only interactive components; Astro's islands ship zero JS for static components and independently hydrate each interactive island.
React 18 hydrates in priority order โ it hydrates the component the user is interacting with first, before completing the rest of the tree.
Qwik serialises component state and event handler references into HTML so the client resumes where the server left off โ no re-execution of component code on load.
The page is in the hydration gap โ HTML is visible but React hasn't attached event listeners yet. Fix with streaming SSR, selective hydration, or partial hydration.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.How does Next.js App Router handle hydration for Server vs Client Components?