facebook/react · error · Error

537

537

Error message

Cannot pass event handlers (${propKey}) in renderToHTML because the HTML will never be hydrated so they can never get called.

What it means

experimental_renderToHTML emits inert HTML that will never be hydrated, so event handler props could never be invoked; the markup Fizz config throws whenever any host-element prop is a function, naming the offending propKey in the message. This is a deliberate guard that static output stays non-interactive.

Source

Thrown at packages/react-markup/src/ReactFizzConfigMarkup.js:129

  type: string,
  props: Object,
  resumableState: ResumableState,
  renderState: RenderState,
  preambleState: null | PreambleState,
  hoistableState: null | HoistableState,
  formatContext: FormatContext,
  textEmbedded: boolean,
): ReactNodeList {
  for (const propKey in props) {
    if (hasOwnProperty.call(props, propKey)) {
      const propValue = props[propKey];
      if (propKey === 'ref' && propValue != null) {
        throw new Error(
          'Cannot pass ref in renderToHTML because they will never be hydrated.',
        );
      }
      if (typeof propValue === 'function') {
        throw new Error(
          'Cannot pass event handlers (' +
            propKey +
            ') in renderToHTML because ' +
            'the HTML will never be hydrated so they can never get called.',
        );
      }
    }
  }

  return pushStartInstanceImpl(
    target,
    type,
    props,
    resumableState,
    renderState,
    preambleState,
    hoistableState,
    formatContext,

View on GitHub (pinned to eafeac097b)

Solutions

  1. Remove function props from all elements rendered by renderToHTML.
  2. Branch in shared components on a static/isStatic flag to omit handlers during static rendering.
  3. Attach interactivity client-side: emit data-* attributes and call addEventListener after the HTML string is inserted into the document.

Example fix

// before
const html = await experimental_renderToHTML(
  <button onClick={save}>Save</button>,
);

// after
const html = await experimental_renderToHTML(
  <button data-action='save'>Save</button>,
);
// after insertion:
container
  .querySelector('[data-action=save]')
  .addEventListener('click', save);
Defensive patterns

Strategy: validation

Validate before calling

import {cloneElement, isValidElement} from 'react';
function forStaticMarkup(node) {
  if (Array.isArray(node)) return node.map(forStaticMarkup);
  if (!isValidElement(node)) return node;
  const next = {};
  for (const key in node.props) {
    const value = node.props[key];
    if (key === 'ref' || typeof value === 'function') continue;
    next[key] =
      key === 'children' && value != null ? forStaticMarkup(value) : value;
  }
  return cloneElement(node, next);
}
const html = await experimental_renderToHTML(forStaticMarkup(<App />));

Prevention

When it happens

Trigger: Passing a function prop (onClick, onChange, onSubmit, onPress, ...) to any host element inside the renderToHTML tree - including handlers deep inside reused client components or spread props objects.

Common situations: Dropping a client page/component into a static markup render; components with built-in onSubmit/onClick; spreading untyped props that carry handlers; migrating hydrated SSR code to renderToHTML.

Related errors


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