facebook/react · error · Error

536

536

Error message

Cannot pass ref in renderToHTML because they will never be hydrated.

What it means

react-markup's experimental_renderToHTML produces plain HTML that is never hydrated, so a ref could never be attached; the markup Fizz host config (ReactFizzConfigMarkup) hard-throws at pushStart when any host element carries a non-null ref prop. The throw happens during rendering and rejects the renderToHTML promise via the destination's destroy path.

Source

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

  return parentContext;
}

export function pushStartInstance(
  target: Array<Chunk | PrecomputedChunk>,
  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,

View on GitHub (pinned to eafeac097b)

Solutions

  1. Remove ref from every element passed to renderToHTML.
  2. In shared components, make the ref conditional (skip it when rendering static markup, e.g. behind an isStatic prop or a separate code path).
  3. If you need element access, insert the returned HTML string client-side and use querySelector instead of a ref.

Example fix

// before
await experimental_renderToHTML(<input ref={inputRef} />);

// after
const html = await experimental_renderToHTML(<input />);
// after inserting the HTML:
container.insertAdjacentHTML('beforeend', html);
container.querySelector('input').focus();
Defensive patterns

Strategy: validation

Validate before calling

import {cloneElement, isValidElement} from 'react';
function stripRefs(node) {
  if (Array.isArray(node)) return node.map(stripRefs);
  if (!isValidElement(node)) return node;
  const {ref, ...props} = node.props; // eslint-disable-line no-unused-vars
  if (props.children != null) props.children = stripRefs(props.children);
  return cloneElement(node, {...props, ref: null});
}
const html = await experimental_renderToHTML(stripRefs(<App />));

Prevention

When it happens

Trigger: Passing ref to any host element inside the tree given to experimental_renderToHTML - including refs added inside shared design-system components, inputs, or HOCs that unconditionally forward refs.

Common situations: Reusing client components or pages in a static-markup pipeline; component libraries whose inputs/buttons always attach refs; migrating from ReactDOMServer.renderToStaticMarkup to renderToHTML without removing refs.

Related errors


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