testing-library/react-testing-library · error · Error

Attempted to hydrate a non-hydrateable root. This is a bug i

Error message

Attempted to hydrate a non-hydrateable root. This is a bug in `@testing-library/react`.

What it means

In createConcurrentRoot (src/pure.js), the returned root object exposes a hydrate() method that is only meaningful when the root was created via hydrateRoot. If the internal `hydrate` flag is false the root was built with createRoot, so calling hydrate() throws. The message says 'This is a bug in `@testing-library/react`' because the library itself should never call hydrate on a non-hydrateable root; users cannot normally trigger it through the public API.

Source

Thrown at src/pure.js:136

        strictModeIfNeeded(
          wrapUiIfNeeded(ui, WrapperComponent),
          reactStrictMode,
        ),
        {onCaughtError, onRecoverableError},
      )
    })
  } else {
    root = ReactDOMClient.createRoot(container, {
      onCaughtError,
      onRecoverableError,
    })
  }

  return {
    hydrate() {
      /* istanbul ignore if */
      if (!hydrate) {
        throw new Error(
          'Attempted to hydrate a non-hydrateable root. This is a bug in `@testing-library/react`.',
        )
      }
      // Nothing to do since hydration happens when creating the root object.
    },
    render(element) {
      root.render(element)
    },
    unmount() {
      root.unmount()
    },
  }
}

function createLegacyRoot(container) {
  return {
    hydrate(element) {
      ReactDOM.hydrate(element, container)

View on GitHub (pinned to 20ce75f290)

Solutions

  1. Pass {hydrate: true} to render() if you intend to hydrate server-rendered markup, so the root is created with hydrateRoot.
  2. Do not call (or trigger) hydrate() on a root created without the hydrate option; use render() instead.
  3. If it appears without custom code, report it as a bug to @testing-library/react with a reproduction.
  4. Verify you are not mixing roots created by different render() calls (hydrated vs non-hydrated) in a custom wrapper.

Example fix

// before
render(<App />, { container })
root.hydrate()
// after
render(<App />, { container, hydrate: true })
Defensive patterns

Strategy: validation

Validate before calling

// Only trigger hydration when you opted in
const options = isServerMarkup ? { hydrate: true } : {}
render(<App />, options)

Type guard

function canHydrate(renderOptions) {
  return typeof renderOptions === 'object' && renderOptions !== null && renderOptions.hydrate === true
}

Try / catch

try {
  render(<App />, { hydrate: true })
} catch (e) {
  if (String(e.message).includes('non-hydrateable root')) {
    // retry as a plain client render
    render(<App />)
  } else {
    throw e
  }
}

Prevention

When it happens

Trigger: renderRoot calls root.hydrate() on a root object produced by createConcurrentRoot with hydrate: false (i.e. render() was called without {hydrate: true}), a state inconsistency inside the library.

Common situations: Practically only seen when a custom wrapper or patched internals of @testing-library/react mix hydrated and non-hydrated roots, or when passing an externally created root object into renderRoot with mismatched options.

Related errors


AI-assisted analysis of testing-library/react-testing-library@20ce75f290 (2026-09-02). Data as JSON: /api/errors/a8d5c05367e2336d. Report an issue: GitHub.