or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

client-rendering.mdform-actions.mdindex.mdresource-hints.mdserver-rendering.mdstatic-generation.md
tile.json

client-rendering.mddocs/

Client-Side Rendering

Client-side rendering APIs for modern React applications using concurrent rendering features.

Capabilities

createRoot

Creates a React root for rendering React components inside a browser DOM element with concurrent features enabled.

/**
 * Create a React root for concurrent rendering
 * @param container - DOM element or DocumentFragment to render into
 * @param options - Optional configuration for the root
 * @returns Root object with render() and unmount() methods
 */
function createRoot(
  container: Element | DocumentFragment,
  options?: CreateRootOptions
): Root;

interface CreateRootOptions {
  /** Enable React strict mode checks */
  unstable_strictMode?: boolean;
  /** Prefix for IDs generated by useId hook */
  identifierPrefix?: string;
  /** Handler for uncaught errors that escape all error boundaries */
  onUncaughtError?: (error: Error, errorInfo: ErrorInfo) => void;
  /** Handler for errors caught by error boundaries */
  onCaughtError?: (error: Error, errorInfo: ErrorInfo) => void;
  /** Handler for errors React can recover from automatically */
  onRecoverableError?: (error: Error, errorInfo: ErrorInfo) => void;
  /** Experimental callback for default transition indicator (can return cleanup function) */
  onDefaultTransitionIndicator?: () => void | (() => void);
  /** Experimental transition callbacks for React DevTools */
  unstable_transitionCallbacks?: TransitionTracingCallbacks;
}

interface Root {
  /** Render React elements into the root */
  render(children: ReactNode): void;
  /** Unmount the root and cleanup all resources */
  unmount(): void;
}

interface ErrorInfo {
  /** Stack trace of React components involved in the error */
  componentStack?: string;
  /** The error boundary component that caught the error (if onCaughtError) */
  errorBoundary?: React.Component;
}

Usage Examples:

import { createRoot } from 'react-dom/client';

// Basic usage
const root = createRoot(document.getElementById('root'));
root.render(<App />);

// With options
const root = createRoot(document.getElementById('root'), {
  identifierPrefix: 'my-app-',
  onUncaughtError(error, errorInfo) {
    console.error('Uncaught error:', error);
    console.error('Component stack:', errorInfo.componentStack);
    // Report to error tracking service
  },
  onRecoverableError(error, errorInfo) {
    console.warn('Recoverable error:', error);
    // Log but don't necessarily show to user
  }
});
root.render(<App />);

// Update rendered content
root.render(<App updatedProp="value" />);

// Cleanup when done
root.unmount();

Key Features:

  • Concurrent Rendering: Enables React 18's concurrent features like useTransition, useDeferredValue
  • Automatic Batching: All state updates are automatically batched for better performance
  • Error Handling: Configure custom error handlers for different error scenarios
  • Progressive Updates: React can interrupt long-running renders to handle urgent updates
  • Suspense Support: Full support for Suspense boundaries and lazy loading

Migration from React 17:

// Old (React 17)
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));

// New (React 18+)
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);

Notes:

  • The container's existing children will be replaced when you call render() for the first time
  • Calling render() multiple times updates the existing render
  • unmount() removes all event handlers and state, and stops all pending work
  • After calling unmount(), you cannot use the root again; create a new root if needed

hydrateRoot

Attaches React to server-rendered HTML inside a browser DOM element, preserving the existing markup and making it interactive.

/**
 * Hydrate server-rendered HTML with React
 * @param container - DOM element containing server-rendered content
 * @param initialChildren - React elements matching the server content
 * @param options - Optional configuration for hydration
 * @returns Root object with render() and unmount() methods
 */
function hydrateRoot(
  container: Element | Document,
  initialChildren: ReactNode,
  options?: HydrateRootOptions
): Root;

interface HydrateRootOptions extends CreateRootOptions {
  /** Called when a Suspense boundary completes hydration */
  onHydrated?: (suspenseBoundary: SuspenseBoundary) => void;
  /** Called when a Suspense boundary is deleted during hydration */
  onDeleted?: (suspenseBoundary: SuspenseBoundary) => void;
  /** Form state from server rendering (for Server Actions) */
  formState?: ReactFormState;
}

type SuspenseBoundary = Comment; // HTML comment node marking boundary

Usage Examples:

import { hydrateRoot } from 'react-dom/client';

// Basic hydration
hydrateRoot(document.getElementById('root'), <App />);

// With options
hydrateRoot(
  document.getElementById('root'),
  <App />,
  {
    onRecoverableError(error, errorInfo) {
      console.error('Hydration error:', error);
      // Log hydration mismatches
    },
    onHydrated(suspenseBoundary) {
      console.log('Suspense boundary hydrated:', suspenseBoundary);
    }
  }
);

// Hydrating the entire document
hydrateRoot(document, <App />);

Hydration with Server Actions:

import { hydrateRoot } from 'react-dom/client';

// Pass formState from server to enable Server Actions
const formState = window.__SERVER_FORM_STATE__;
hydrateRoot(
  document.getElementById('root'),
  <App />,
  { formState }
);

Key Features:

  • Preserves Markup: Reuses existing server-rendered HTML instead of recreating it
  • Fast Initial Render: Page is visible and interactive faster than client-only rendering
  • Progressive Hydration: Can hydrate in chunks with Suspense boundaries
  • Error Recovery: Automatically recovers from minor hydration mismatches
  • Server Actions Support: Can hydrate forms with Server Actions

Hydration Process:

  1. React attaches event listeners to existing DOM nodes
  2. Runs effects and state initialization
  3. Verifies server and client content match
  4. Makes the page interactive
  5. Continues hydrating deferred content in Suspense boundaries

Common Hydration Issues:

// ❌ Problem: Mismatch between server and client
function Component() {
  // Date.now() or Math.random() will differ between server and client
  return <div>{Date.now()}</div>;
}

// ✅ Solution: Use useEffect for client-only content
function Component() {
  const [date, setDate] = useState(null);

  useEffect(() => {
    setDate(Date.now());
  }, []);

  return <div>{date ?? 'Loading...'}</div>;
}

// ✅ Solution: Suppress hydration warning for known mismatches
function Component() {
  return <div suppressHydrationWarning>{Date.now()}</div>;
}

Best Practices:

  1. Ensure Identical Content: Server and client must render the same content initially
  2. Handle Client-Only Content: Use useEffect or useState for client-only data
  3. Use suppressHydrationWarning: Only for intentional mismatches like timestamps
  4. Monitor Errors: Configure onRecoverableError to track hydration issues
  5. Progressive Hydration: Wrap deferred content in <Suspense> boundaries

Notes:

  • Hydration warnings indicate server/client mismatches that can cause bugs
  • React will try to recover from hydration errors by client-side rendering
  • Hydration is synchronous - the page will be blocked until React attaches
  • Use <Suspense> for progressive/selective hydration of deferred content

Root Methods

Both createRoot and hydrateRoot return a Root object with these methods:

render

/**
 * Render or update React elements in the root
 * @param children - React elements to render
 */
root.render(children: ReactNode): void;

Usage:

const root = createRoot(document.getElementById('root'));

// Initial render
root.render(<App />);

// Update render
root.render(<App newProp="value" />);

// Render different content
root.render(<DifferentApp />);

// Remove content (alternative to unmount)
root.render(null);

Notes:

  • First call replaces all existing children in the container
  • Subsequent calls update the existing render
  • React efficiently updates only what changed
  • Rendering null removes all content but keeps the root mounted

unmount

/**
 * Unmount the root and cleanup all resources
 */
root.unmount(): void;

Usage:

const root = createRoot(document.getElementById('root'));
root.render(<App />);

// Cleanup
root.unmount();

// Container is now empty and all event handlers are removed

Notes:

  • Calls cleanup functions from all effects
  • Removes all event listeners
  • Stops all pending async work
  • Clears all state and references
  • Cannot reuse the root after unmounting; create a new one if needed

Error Handling

React DOM provides multiple error handlers for different scenarios:

onUncaughtError

Called when an error escapes all error boundaries and reaches the root.

createRoot(container, {
  onUncaughtError(error, errorInfo) {
    // Error that crashed the entire app
    console.error('Fatal error:', error);
    reportToErrorService(error, errorInfo.componentStack);
  }
});

onCaughtError

Called when an error is caught by an error boundary.

createRoot(container, {
  onCaughtError(error, errorInfo) {
    // Error caught by error boundary
    console.log('Caught error:', error);
    console.log('Boundary:', errorInfo.errorBoundary);
    logNonFatalError(error);
  }
});

onRecoverableError

Called when React automatically recovers from an error (e.g., hydration mismatch).

createRoot(container, {
  onRecoverableError(error, errorInfo) {
    // React recovered but you should still log it
    console.warn('Recovered from:', error);
    logRecoverableError(error, errorInfo.componentStack);
  }
});

Performance Considerations

Identifier Prefix

Use identifierPrefix when rendering multiple React roots on the same page to avoid ID collisions:

const headerRoot = createRoot(document.getElementById('header'), {
  identifierPrefix: 'header-'
});

const mainRoot = createRoot(document.getElementById('main'), {
  identifierPrefix: 'main-'
});

// IDs from useId will be prefixed: 'header-:r1:', 'main-:r1:', etc.

Concurrent Features

createRoot enables concurrent features that improve responsiveness:

import { useState, useTransition } from 'react';

function SearchResults() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    // Urgent: show what was typed
    setQuery(e.target.value);

    // Non-urgent: update search results
    startTransition(() => {
      setResults(searchResults(e.target.value));
    });
  }

  return <input value={query} onChange={handleChange} />;
}

Version

const version: string; // "19.2.0"

All client rendering APIs export the React DOM version string.

Types

ReactNode

type ReactNode =
  | ReactElement
  | string
  | number
  | boolean
  | null
  | undefined
  | ReactNode[];

TransitionTracingCallbacks

interface TransitionTracingCallbacks {
  onTransitionStart?: (transitionName: string, startTime: number) => void;
  onTransitionProgress?: (
    transitionName: string,
    startTime: number,
    currentTime: number,
    pending: Array<{ name: string }>
  ) => void;
  onTransitionComplete?: (
    transitionName: string,
    startTime: number,
    endTime: number
  ) => void;
  onMarkerProgress?: (
    transitionName: string,
    marker: string,
    startTime: number,
    currentTime: number,
    pending: Array<{ name: string }>
  ) => void;
  onMarkerComplete?: (
    transitionName: string,
    marker: string,
    startTime: number,
    endTime: number
  ) => void;
}

Note: These callbacks are unstable and primarily used by React DevTools for transition profiling.