Skip to main content

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)
reactNode
ReactNode
required
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.
options
HydrateRootOptions
Optional configuration object for hydration.

Returns

root
RootType
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.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

  1. Incorrect nesting - Invalid HTML structure
  2. Browser extensions - Modify the DOM
  3. Date/time - Server and client timestamps differ
  4. Random values - Using Math.random() or Date.now()
  5. 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 />
);

Performance Optimization

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