What is the Difference Between Hydration and Initial Rendering in React?
I kept getting hydration mismatch errors in my Next.js app. The error message said “server rendered HTML didn’t match the client” but I couldn’t figure out why. Digging into React internals revealed something unexpected: hydration doesn’t create DOM nodes at all.
The Core Difference
When React renders initially, it calls document.createElement() for every host component and inserts those nodes into the DOM. During hydration, React skips this step entirely. It finds existing DOM nodes from server-side rendered HTML and creates fiber nodes pointing to them.
Initial mount: fiber.stateNode = document.createElement('div') + DOM insertionHydration: fiber.stateNode = existingDOMNode (no createElement, no insertion)This distinction explains why hydration errors occur and why certain patterns break SSR.
Initial Rendering: Create and Insert
When I call ReactDOM.render() or createRoot().render() on an empty container, React goes through two phases.
Render Phase:
React builds the fiber tree from React elements. Since there’s no existing DOM, no diffing is needed. All fibers get marked with Placement effect tag.
Commit Phase:
For each HostComponent (div, span, etc.), React’s completeWork function:
1. document.createElement(type) -> CREATE DOM node2. setInitialProperties(domNode) -> Set className, style, etc.3. fiber.stateNode = domNode -> Link fiber to DOM4. parentDomNode.appendChild(domNode) -> INSERT into DOM treeThe fiber node ends up like this:
const fiber = { tag: HostComponent, // 5 type: 'div', stateNode: document.createElement('div'), // NEW DOM element effectTag: Placement // 1 (insert into DOM)}Hydration: Reuse and Attach
When I use hydrateRoot() after SSR, the process differs critically in the commit phase.
Render Phase:
Same as initial render - builds fiber tree. But React also tracks which DOM nodes already exist from the server HTML.
Commit Phase (The Difference):
For each HostComponent, React’s completeWork does something completely different:
1. findExistingDOMNode(fiber) -> LOCATE existing DOM (no createElement!)2. verifyMatch(domNode, fiber) -> Check if matches expectations3. fiber.stateNode = domNode -> Link fiber to existing DOM4. attachEventHandlers(domNode) -> Add click handlers, etc.5. NO appendChild -> Node already in DOM!The fiber node references existing DOM:
const fiber = { tag: HostComponent, type: 'div', stateNode: existingDivElement, // REFERENCE to SSR DOM effectTag: Hydrating // Different effect tag}Visualizing the Two Processes
┌───────────────────────────────────────────────────────────────┐│ INITIAL RENDER (ReactDOM.render) │├───────────────────────────────────────────────────────────────┤│ ││ React Elements ││ │ ││ ▼ ││ Render Phase: Build Fiber Tree ││ │ ││ ▼ ││ Commit Phase: ││ ┌──────────────────────────────────┐ ││ │ document.createElement() │── CREATE DOM ││ │ fiber.stateNode = newDOM │ ││ │ parent.appendChild(newDOM) │── INSERT DOM ││ └──────────────────────────────────┘ ││ │└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐│ HYDRATION (hydrateRoot) │├───────────────────────────────────────────────────────────────┤│ ││ Server HTML (already in DOM) ││ │ ││ ▼ ││ Render Phase: Build Fiber Tree ││ │ ││ ▼ ││ Commit Phase: ││ ┌──────────────────────────────────┐ ││ │ findExistingDOMNode() │── REUSE DOM ││ │ fiber.stateNode = existingDOM │ ││ │ attachEventHandlers() │── ATTACH EVENTS ││ │ NO createElement() │ ││ │ NO appendChild() │ ││ └──────────────────────────────────┘ ││ │└───────────────────────────────────────────────────────────────┘Why Hydration Errors Occur
I got hydration mismatch errors because React expects the server HTML to match exactly what it computes on the client. If they differ, hydration fails.
Server renders: <div class="server-class">Hello</div>Client expects: <div class="client-class">Hello</div>
React throws: "Hydration failed because server HTML didn't match"Common causes I encountered:
- Browser-only APIs during render: Using
window.innerWidthorDate.now()in the component body - Conditional rendering based on client state:
if (typeof window !== 'undefined') - Invalid HTML nesting:
<div>inside<p>- browser auto-fixes it differently - Different data: Server fetch returned different data than client expected
How I Fixed My Hydration Error
My component used new Date().toLocaleTimeString() directly in JSX. The server rendered a timestamp at build/request time, but the client rendered a different timestamp when hydrating.
// WRONG: Server and client times differfunction Clock() { return <div>{new Date().toLocaleTimeString()}</div>}I moved the dynamic value to useEffect:
// RIGHT: Same initial value, updates after hydrationfunction Clock() { const [time, setTime] = useState('')
useEffect(() => { setTime(new Date().toLocaleTimeString()) const interval = setInterval(() => { setTime(new Date().toLocaleTimeString()) }, 1000) return () => clearInterval(interval) }, [])
return <div>{time || 'Loading...'}</div>}The server renders “Loading…”, client hydrates with same initial value, then useEffect updates to actual time.
Terminology Clarification
The confusion started with what “render” and “create” mean in React context.
“Render” in React = calculate, not paint:
The render phase computes what the UI should look like. It builds the fiber tree. It doesn’t touch the actual DOM or paint anything visually.
“Create” = call document.createElement():
When I say React “creates” a DOM node, I mean it literally calls the browser’s document.createElement() API and inserts that node into the DOM tree. Hydration skips this because the node already exists.
Render Phase -> Calculate fiber tree (both initial and hydration)Commit Phase -> Apply changes to actual DOMCreate -> document.createElement() + appendChild()Hydrate -> Find existing DOM + attach event handlersFiber’s stateNode Property
Each fiber node has a stateNode property. What it holds depends on the fiber type:
HostComponent (div, span) -> Actual DOM elementClassComponent -> Component instanceFunctionComponent -> null (no instance)HostRoot -> FiberRoot (container info)During initial render, stateNode gets a freshly created DOM element. During hydration, stateNode gets a reference to an existing DOM element from SSR.
Practical Implications
Understanding this difference changed how I approach SSR:
1. Never use browser APIs in render body
// WRONG: window is undefined on serverfunction Component() { const width = window.innerWidth return <div style={{ width }}>{width}px</div>}2. Use useEffect for client-only logic
// RIGHT: useEffect runs only on client, after hydrationfunction Component() { const [width, setWidth] = useState(0)
useEffect(() => { setWidth(window.innerWidth) }, [])
return <div style={{ width }}>{width}px</div>}3. Ensure consistent data between server and client
If I fetch data on server, I must pass the same data to the client via props or hydration context. Different data means mismatch.
4. Validate HTML structure
Invalid nesting like <div> inside <p> causes the browser to “fix” the HTML differently on server vs client, leading to mismatches.
Key Takeaways
- Initial rendering creates DOM nodes - React calls
document.createElement()for each host component - Hydration reuses existing DOM - React finds SSR nodes and attaches fiber nodes to them, no creation
- Render means calculate - Not visual paint, just building the fiber tree
- Hydration errors are mismatch errors - Server HTML must match client expectations exactly
- Use useEffect for browser APIs - Not in render body, to avoid server/client differences
The distinction between “creating” DOM nodes and “adopting” existing ones is the key to understanding React’s SSR hydration. Once I grasped this, hydration errors became predictable and fixable.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Inside React - Deep Dive into React Internals
- 👨💻 React Fiber Architecture
- 👨💻 React SSR Hydration Documentation
- 👨💻 Hydration Mismatch Errors
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments