facebook/react · error · Error

318

318

Error message

A dehydrated suspense component was completed without a hydrated node. This is probably a bug in React.

What it means

completeDehydratedActivityBoundary() finishes hydrating an <Activity> boundary that kept its dehydrated state (nextState non-null). On the first pass (current === null), popHydrationState() must report that a hydrated node was adopted; if it reports none while the state says dehydrated, the server-rendered DOM for the boundary is missing, and React throws rather than build a corrupted tree.

Source

Thrown at packages/react-reconciler/src/ReactFiberCompleteWork.js:928

  completedWork.childLanes = newChildLanes;

  return didBailout;
}

function completeDehydratedActivityBoundary(
  current: Fiber | null,
  workInProgress: Fiber,
  nextState: ActivityState | null,
): boolean {
  const wasHydrated = popHydrationState(workInProgress);

  if (nextState !== null) {
    // We might be inside a hydration state the first time we're picking up this
    // Activity boundary, and also after we've reentered it for further hydration.
    if (current === null) {
      if (!wasHydrated) {
        throw new Error(
          'A dehydrated suspense component was completed without a hydrated node. ' +
            'This is probably a bug in React.',
        );
      }
      prepareToHydrateHostActivityInstance(workInProgress);
      bubbleProperties(workInProgress);
      if (enableProfilerTimer) {
        if ((workInProgress.mode & ProfileMode) !== NoMode) {
          // $FlowFixMe[invalid-compare]
          const isTimedOutSuspense = nextState !== null;
          if (isTimedOutSuspense) {
            // Don't count time spent in a timed out Suspense subtree as part of the base duration.
            const primaryChildFragment = workInProgress.child;
            if (primaryChildFragment !== null) {
              // $FlowFixMe[unsafe-arithmetic] Flow doesn't support type casting in combination with the -= operator
              workInProgress.treeBaseDuration -=
                primaryChildFragment.treeBaseDuration as any as number;
            }

View on GitHub (pinned to eafeac097b)

Solutions

  1. Run the identical React version on server and client so both agree on what Activity emits
  2. Diff the DOM at hydration time against the SSR output to confirm the boundary markup still exists
  3. Disable DOM-rewriting scripts and browser extensions on the page and retest
  4. If the HTML is intact and it still throws, file an issue with the SSR fragment and the hydration call
Defensive patterns

Strategy: fallback

Validate before calling

// Detect tampered SSR markup before hydrating
const container = document.getElementById('app');
if (!container || container.childElementCount === 0) {
  createRoot(container ?? document.createElement('div')).render(<App />);
} else {
  hydrateRoot(container, <App />);
}

Try / catch

hydrateRoot(container, <App />, {
  onUncaughtError(error, info) {
    if (/completed without a hydrated node/.test(String(error))) {
      reportError(error, info.componentStack);
      container.textContent = '';
      createRoot(container).render(<App />); // last-resort client re-render
    }
  },
});

Prevention

When it happens

Trigger: hydrateRoot() over HTML whose Activity boundary markup was removed or restructured before hydration - DOM stripped by scripts or extensions, streamed HTML truncated, or a hydration mismatch inside the boundary that popped the hydration state without adopting a node; also internal hydration bugs specific to Activity.

Common situations: SSR with <Activity mode="hidden"> where the hidden HTML was never emitted (server on an older React); browser extensions or in-page scripts rewriting the DOM; edge/CDM transformers mangling streamed markup; React canary after Activity shipped.

Related errors


AI-assisted analysis of facebook/react@eafeac097b (2026-08-21). Data as JSON: /api/errors/41832b315978ab44. Report an issue: GitHub.