Skip to content

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.

The key distinction
Initial mount: fiber.stateNode = document.createElement('div') + DOM insertion
Hydration: 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:

Commit phase operations
1. document.createElement(type) -> CREATE DOM node
2. setInitialProperties(domNode) -> Set className, style, etc.
3. fiber.stateNode = domNode -> Link fiber to DOM
4. parentDomNode.appendChild(domNode) -> INSERT into DOM tree

The fiber node ends up like this:

Fiber after initial render
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:

Hydration commit phase
1. findExistingDOMNode(fiber) -> LOCATE existing DOM (no createElement!)
2. verifyMatch(domNode, fiber) -> Check if matches expectations
3. fiber.stateNode = domNode -> Link fiber to existing DOM
4. attachEventHandlers(domNode) -> Add click handlers, etc.
5. NO appendChild -> Node already in DOM!

The fiber node references existing DOM:

Fiber after hydration
const fiber = {
tag: HostComponent,
type: 'div',
stateNode: existingDivElement, // REFERENCE to SSR DOM
effectTag: Hydrating // Different effect tag
}

Visualizing the Two Processes

Initial render vs Hydration flow
┌───────────────────────────────────────────────────────────────┐
│ 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.

Mismatch example
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:

  1. Browser-only APIs during render: Using window.innerWidth or Date.now() in the component body
  2. Conditional rendering based on client state: if (typeof window !== 'undefined')
  3. Invalid HTML nesting: <div> inside <p> - browser auto-fixes it differently
  4. 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: Date during render
// WRONG: Server and client times differ
function Clock() {
return <div>{new Date().toLocaleTimeString()}</div>
}

I moved the dynamic value to useEffect:

Right: Date in useEffect
// RIGHT: Same initial value, updates after hydration
function 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.

Terminology summary
Render Phase -> Calculate fiber tree (both initial and hydration)
Commit Phase -> Apply changes to actual DOM
Create -> document.createElement() + appendChild()
Hydrate -> Find existing DOM + attach event handlers

Fiber’s stateNode Property

Each fiber node has a stateNode property. What it holds depends on the fiber type:

stateNode contents by fiber type
HostComponent (div, span) -> Actual DOM element
ClassComponent -> Component instance
FunctionComponent -> 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 pattern
// WRONG: window is undefined on server
function Component() {
const width = window.innerWidth
return <div style={{ width }}>{width}px</div>
}

2. Use useEffect for client-only logic

Correct pattern
// RIGHT: useEffect runs only on client, after hydration
function 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

  1. Initial rendering creates DOM nodes - React calls document.createElement() for each host component
  2. Hydration reuses existing DOM - React finds SSR nodes and attaches fiber nodes to them, no creation
  3. Render means calculate - Not visual paint, just building the fiber tree
  4. Hydration errors are mismatch errors - Server HTML must match client expectations exactly
  5. 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments