hydrateRoot
Hydrates a React application in a browser DOM container that was previously rendered by a React server API.
import { hydrateRoot } from 'react-dom/client' ;
const root = hydrateRoot ( container , reactNode , options );
Reference
hydrateRoot(container, reactNode, options?)
Call hydrateRoot to attach React to existing HTML that was rendered by React in a server environment.
/src/client/ReactDOMRoot.js:274-358
const root = hydrateRoot (
document . getElementById ( 'root' ),
< App />
);
Parameters
container
Document | Element
required
The DOM element that contains the server-rendered HTML. Must match the container used on the server. Validation:
Must be a valid DOM element or Document
Should contain server-rendered React HTML
Cannot be a DocumentFragment (unlike createRoot)
The React node that was used to render the existing HTML on the server. This must be identical to what was rendered on the server. // Server and client must render the same tree
hydrateRoot ( container , < App /> );
The reactNode must be identical to what was rendered on the server. Differences will cause hydration warnings or errors.
Optional configuration object for hydration. onRecoverableError
(error: Error, errorInfo: ErrorInfo) => void
Called when React recovers from errors during hydration, such as hydration mismatches. hydrateRoot ( container , < App /> , {
onRecoverableError : ( error , errorInfo ) => {
console . warn ( 'Hydration mismatch:' , error . message );
logToAnalytics ( 'hydration-error' , {
message: error . message ,
componentStack: errorInfo . componentStack ,
});
}
});
onHydrated
(suspenseBoundary: Comment) => void
Called when a Suspense boundary successfully hydrates on the client. hydrateRoot ( container , < App /> , {
onHydrated : ( boundary ) => {
console . log ( 'Suspense boundary hydrated:' , boundary );
}
});
onDeleted
(suspenseBoundary: Comment) => void
Called when a Suspense boundary is deleted from the client tree because it wasn’t needed. hydrateRoot ( container , < App /> , {
onDeleted : ( boundary ) => {
console . log ( 'Suspense boundary deleted:' , boundary );
}
});
onUncaughtError
(error: Error, errorInfo: ErrorInfo) => void
Called when an error is thrown and not caught by any error boundary.
onCaughtError
(error: Error, errorInfo: ErrorInfo) => void
Called when an error boundary catches an error.
String prefix for IDs generated by useId. Must match the prefix used during server rendering. // Server
const html = await renderToString ( < App /> , {
identifierPrefix: 'app-'
});
// Client
hydrateRoot ( container , < App /> , {
identifierPrefix: 'app-'
});
Form state to restore after hydration. Used with server actions and progressive enhancement.
Enable Strict Mode for this root.
unstable_transitionCallbacks
TransitionTracingCallbacks
Callbacks for transition tracing.
Returns
A root object with render, unmount, and unstable_scheduleHydration methods.
root.render(reactNode)
Updates the hydrated root after initial hydration. Works the same as createRoot().render().
root . render ( < App theme = "dark" /> );
root.unmount()
Unmounts the React tree and cleans up. Same behavior as createRoot().unmount().
root.unstable_scheduleHydration(target)
Schedules eager hydration of a specific DOM node.
root . unstable_scheduleHydration ( document . getElementById ( 'lazy-section' ));
This is an unstable API and may change in future versions.
Usage Examples
Basic SSR Hydration
Server (Node.js):
import { renderToPipeableStream } from 'react-dom/server' ;
import App from './App' ;
const { pipe } = renderToPipeableStream ( < App /> , {
onShellReady () {
res . setHeader ( 'content-type' , 'text/html' );
pipe ( res );
}
});
Client:
import { hydrateRoot } from 'react-dom/client' ;
import App from './App' ;
hydrateRoot ( document . getElementById ( 'root' ), < App /> );
With Error Tracking
import { hydrateRoot } from 'react-dom/client' ;
import * as Sentry from '@sentry/react' ;
const root = hydrateRoot (
document . getElementById ( 'root' ),
< App /> ,
{
onRecoverableError : ( error , errorInfo ) => {
// Log hydration mismatches
if ( error . message . includes ( 'Hydration' )) {
Sentry . captureException ( error , {
tags: { type: 'hydration-mismatch' },
contexts: {
react: {
componentStack: errorInfo . componentStack ,
},
},
});
}
},
}
);
With Suspense Boundaries
import { hydrateRoot } from 'react-dom/client' ;
import { Suspense } from 'react' ;
function App () {
return (
< div >
< h1 > My App </ h1 >
< Suspense fallback = { < Spinner /> } >
< LazyComponent />
</ Suspense >
</ div >
);
}
const root = hydrateRoot (
document . getElementById ( 'root' ),
< App /> ,
{
onHydrated : ( boundary ) => {
console . log ( 'Suspense boundary hydrated' );
},
}
);
Progressive Hydration
import { hydrateRoot } from 'react-dom/client' ;
import { useState , useEffect } from 'react' ;
function App () {
const [ isHydrated , setIsHydrated ] = useState ( false );
useEffect (() => {
setIsHydrated ( true );
}, []);
return (
< div >
< header > Static Header (hydrated immediately) </ header >
{ isHydrated && (
< main >
< InteractiveContent />
</ main >
) }
< footer > Static Footer </ footer >
</ div >
);
}
const root = hydrateRoot ( document . getElementById ( 'root' ), < App /> );
// Manually schedule hydration of specific elements
const lazySection = document . getElementById ( 'lazy-section' );
if ( lazySection ) {
root . unstable_scheduleHydration ( lazySection );
}
Handling Hydration Mismatches
Hydration mismatches occur when the server-rendered HTML doesn’t match the client-rendered React tree.
Common Causes
Incorrect nesting - Invalid HTML structure
Browser extensions - Modify the DOM
Date/time - Server and client timestamps differ
Random values - Using Math.random() or Date.now()
Environment differences - window/document available only on client
Example: Date/Time Mismatch
Problem:
function ServerTime () {
// This will mismatch because server and client times differ
return < div > {new Date (). toLocaleTimeString () } </ div > ;
}
Solution 1: Suppress hydration warning
function ServerTime () {
return (
< div suppressHydrationWarning >
{new Date (). toLocaleTimeString () }
</ div >
);
}
Solution 2: Use effect to update on client
import { useState , useEffect } from 'react' ;
function ServerTime () {
const [ time , setTime ] = useState ( null );
useEffect (() => {
setTime ( new Date (). toLocaleTimeString ());
}, []);
// Render nothing on server, time on client after hydration
return < div > { time || '...' } </ div > ;
}
Solution 3: Two-pass rendering
import { useState , useEffect } from 'react' ;
function ServerTime ({ serverTime }) {
const [ isClient , setIsClient ] = useState ( false );
useEffect (() => {
setIsClient ( true );
}, []);
return (
< div >
{ isClient ? new Date (). toLocaleTimeString () : serverTime }
</ div >
);
}
Debugging Hydration Errors
Enable detailed hydration logging in development:
// Hydration warnings will show in console
hydrateRoot ( container , < App /> , {
onRecoverableError : ( error , errorInfo ) => {
console . group ( 'Hydration Error' );
console . error ( error );
console . log ( 'Component stack:' , errorInfo . componentStack );
console . groupEnd ();
},
});
React will log:
Which component caused the mismatch
What the server rendered
What the client expected
The component stack trace
TypeScript
import { hydrateRoot , type Root } from 'react-dom/client' ;
import type { ReactNode } from 'react' ;
interface HydrateRootOptions {
onRecoverableError ?: (
error : unknown ,
errorInfo : { componentStack ?: string }
) => void ;
onHydrated ?: ( suspenseBoundary : Comment ) => void ;
onDeleted ?: ( suspenseBoundary : Comment ) => void ;
identifierPrefix ?: string ;
formState ?: ReactFormState < any , any > | null ;
}
const container = document . getElementById ( 'root' );
if ( ! container ) throw new Error ( 'Root container not found' );
const root : Root = hydrateRoot (
container ,
< App /> ,
{
onRecoverableError : ( error , errorInfo ) => {
console . warn ( 'Hydration error:' , error );
},
}
);
Migration from React 17
Before (React 17)
import ReactDOM from 'react-dom' ;
ReactDOM . hydrate (
< App /> ,
document . getElementById ( 'root' )
);
After (React 18+)
import { hydrateRoot } from 'react-dom/client' ;
hydrateRoot (
document . getElementById ( 'root' ),
< App />
);
Selective Hydration
React 18 automatically prioritizes hydration based on user interactions:
import { Suspense } from 'react' ;
function App () {
return (
< div >
< Header /> { /* Hydrates immediately */ }
< Suspense fallback = { < Spinner /> } >
< Comments /> { /* Hydrates after Header */ }
</ Suspense >
< Suspense fallback = { < Spinner /> } >
< Sidebar /> { /* Hydrates in parallel with Comments */ }
</ Suspense >
</ div >
);
}
hydrateRoot ( document . getElementById ( 'root' ), < App /> );
If user clicks on Sidebar before it hydrates, React will prioritize Sidebar hydration.
Streaming SSR + Selective Hydration
Server:
import { renderToPipeableStream } from 'react-dom/server' ;
const { pipe } = renderToPipeableStream (
< App /> ,
{
onShellReady () {
res . setHeader ( 'content-type' , 'text/html' );
pipe ( res );
},
}
);
Client:
import { hydrateRoot } from 'react-dom/client' ;
// React will stream in content and hydrate progressively
hydrateRoot ( document . getElementById ( 'root' ), < App /> );
Benefits:
HTML streams to browser before all data is ready
Page becomes interactive sooner
React hydrates high-priority content first
Common Pitfalls
Don’t modify DOM before hydration
// Bad: Browser extensions or scripts modify DOM
const root = document . getElementById ( 'root' );
root . innerHTML = '' ; // Don't do this before hydration!
hydrateRoot ( root , < App /> );
// Good: Let React manage the DOM
hydrateRoot ( document . getElementById ( 'root' ), < App /> );
Ensure server/client parity
// Bad: Different content on server vs client
function App () {
// Browser APIs not available on server
const width = window . innerWidth ; // Error on server!
return < div > Width: { width } </ div > ;
}
// Good: Check environment
function App () {
const width = typeof window !== 'undefined' ? window . innerWidth : 0 ;
return < div > Width: { width } </ div > ;
}
// Better: Use effect
function App () {
const [ width , setWidth ] = useState ( 0 );
useEffect (() => {
setWidth ( window . innerWidth );
}, []);
return < div > Width: { width } </ div > ;
}
Browser Compatibility
Same requirements as createRoot:
Modern browsers (Chrome 90+, Firefox 88+, Safari 14.1+, Edge 90+)
ES6 features: Map, Set, Promise
DOM features: Element, Document