Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“Convert a React string to HTML” can describe three different jobs: turning a React element tree into an HTML string, rendering a text value as escaped HTML, or inserting an HTML string that already exists. Use renderToStaticMarkup for non-interactive output, renderToString when the result will be hydrated, and a temporary browser DOM node when conversion must happen in client code. Never treat untrusted HTML as safe merely because it is stored in a JavaScript string.

Choose the operation before choosing an API

What you have and need Recommended approach Key limitation
A React node or component tree that becomes static HTML renderToStaticMarkup The result cannot be hydrated.
Initial server HTML for an interactive app renderToString, then client hydrateRoot It returns immediately; it does not stream or wait for data.
Progressive server output renderToPipeableStream in Node.js or renderToReadableStream in Web Streams runtimes The correct API depends on your runtime.
Static output that waits for data prerender (or its documented Node stream counterpart) It waits for data instead of sending content as it loads.
React rendered in a browser and then read as text Render into a temporary DOM element with createRoot and flushSync, then read innerHTML Do not add server-rendering code to the client bundle just for this.
An existing HTML string to display dangerouslySetInnerHTML only after trust and sanitization checks Untrusted markup can create an XSS vulnerability.

The official API references are renderToStaticMarkup, renderToString, the server API overview, and React’s common component guidance.

Convert a React component to static HTML on the server

For emails, static documents, snapshots, or other markup that will never become a live React application, import renderToStaticMarkup from react-dom/server. It accepts a React node and returns an HTML string.

import { renderToStaticMarkup } from 'react-dom/server';

function Page({ title, items }) {
  return (
    <main>
      <h1>{title}</h1>
      <ul>
        {items.map((item) => <li key={item.id}>{item.name}</li>)}
      </ul>
    </main>
  );
}

const html = renderToStaticMarkup(
  <Page title="Products" items={[{ id: 1, name: 'Keyboard' }]} />
);

console.log(html);
// <main><h1>Products</h1><ul><li>Keyboard</li></ul></main>

Values placed in ordinary JSX children are escaped by React. A title containing <script>, for example, is emitted as text rather than executable markup. The output is non-interactive and, as React documents, cannot be hydrated.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Return a complete document when a consumer needs one

The renderer returns the component’s markup, not automatically a doctype or document shell. Add those yourself when writing a file or sending a complete response.

const body = renderToStaticMarkup(<Page title="Products" items={items} />);
const documentHtml = `<!doctype html>
<html lang="en">
  <head><meta charset="utf-8"><title>Products</title></head>
  <body>${body}</body>
</html>`;

Convert markup that must hydrate into an interactive app

When the browser must attach event handlers and state to the server output, use renderToString and then hydrate the same tree on the client. The server result is only the initial HTML; it is not a replacement for hydration.

// server.js
import { renderToString } from 'react-dom/server';
import App from './App.js';

const html = renderToString(<App initialData={data} />);
// Send html inside your page response.

// browser entry
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(
  document.getElementById('root'),
  <App initialData={window.__INITIAL_DATA__} />
);

renderToString returns immediately. It does not provide streaming or wait for suspended data; when a child suspends, React renders the nearest Suspense fallback. If progressive output or data-aware static generation matters, use the streaming or prerender APIs described in the server API documentation.

When streaming is a better fit

Node.js applications can use renderToPipeableStream; Web Streams runtimes can use renderToReadableStream. These APIs let the response arrive progressively and are preferable when time-to-first-byte and Suspense-driven loading states are part of your server architecture. For output that should wait until data is ready rather than stream partial content, use prerender as documented at React’s prerender reference.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Convert React output in the browser

Do not import react-dom/server into client code simply to serialize a component. React’s documented browser approach is to create a temporary element, render with createRoot, force the update with flushSync, and read innerHTML.

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

export function componentToHtml(node) {
  const container = document.createElement('div');
  const root = createRoot(container);

  flushSync(() => {
    root.render(node);
  });

  const html = container.innerHTML;
  root.unmount();
  return html;
}

const html = componentToHtml(<Card heading="Hello" />);

This is useful when a browser-only value or API is required before serialization. It still produces a snapshot, not a reusable hydrated application. Unmount the temporary root so event handlers and component state are released.

If “React string” means plain text

If your input is a text value, do not parse it as markup. Put it in JSX as a child and let React escape it:

function Message({ text }) {
  return <p>{text}</p>;
}

const html = renderToStaticMarkup(<Message text={'<em>not markup</em>'} />);
// <p>&lt;em&gt;not markup&lt;/em&gt;</p>

This preserves the distinction between content and code. If you need literal formatting, model it as React elements instead of asking a text value to become executable HTML.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Insert an existing HTML string safely

If the string already contains HTML and your goal is to display it inside a React component, use the dangerouslySetInnerHTML escape hatch only with trusted, sanitized content:

function Article({ sanitizedHtml }) {
  const markup = { __html: sanitizedHtml };
  return <article dangerouslySetInnerHTML={markup} />;
}

React warns that HTML based on user input can introduce an XSS vulnerability. Sanitization must happen before insertion, using a policy appropriate to your application; a variable name such as sanitizedHtml is not proof that sanitization occurred. If Trusted Types are enforced, React supports a TrustedHTML value, but the policy creating it must still establish that the content is trusted and sanitized. When the content should remain text, prefer ordinary JSX children.

Common mistakes

  • Calling renderToStaticMarkup and expecting click handlers to work. Static output cannot hydrate.
  • Calling renderToString in browser code. This adds server-rendering code to the client bundle and is not React’s recommended browser conversion path.
  • Passing an HTML string as {html} and expecting tags to render. React escapes it by design.
  • Using dangerouslySetInnerHTML for untrusted comments, profiles, or CMS fields without sanitization.
  • Hydrating markup with a different component tree or data set than the server used, which can produce hydration mismatches.

Troubleshooting conversion failures

“Objects are not valid as a React child”

React children must be renderable values. Convert your data into elements, strings, or numbers, and map arrays with stable keys; do not pass an arbitrary object directly as a child.

The HTML is empty or incomplete

In the browser path, read innerHTML only after flushSync. Without the synchronous flush, the read can occur before React commits the render. Also check that the component does not return null for the supplied props.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Hydration warnings appear

Ensure server and browser render the same tree and initial data. Avoid nondeterministic values such as a fresh timestamp or random number during render; pass a stable value from the server instead.

Suspense content is not present in the string

renderToString does not wait for suspended data and emits the nearest fallback. Use a streaming API or prerendering when your data-loading requirements demand different behavior.

Markup is displayed as text

That is expected when an HTML string is supplied as a normal child. Decide whether it is genuinely trusted markup. If it is not, keep it escaped. If it is, sanitize it and use dangerouslySetInnerHTML deliberately.

Performance, output, and testing considerations

  • Use static rendering for one-time artifacts such as emails and exported documents; it avoids hydration metadata and client startup.
  • Use hydration only when the delivered page needs React interactivity. Sending static markup and then expecting client events without hydrateRoot will not work.
  • Keep serialization deterministic. Stable props, locale, timezone, and ordering make snapshots and hydration safer.
  • Test the returned string at the boundary that consumes it: an email client, file writer, HTTP response, or browser container. Check escaping for quotes, ampersands, angle brackets, and apostrophes.
  • For untrusted rich text, test sanitizer rules with malicious-looking attributes and URLs, not only harmless paragraphs.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your actual goal is to capture the rendered page as an image or PDF, ScreenshotNeo can do the browser work through one request. It accepts consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be switched off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing result in X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

See the ScreenshotNeo documentation for all options. A cURL request is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account to try it.

Frequently Asked Questions

Can I convert a React component to HTML without rendering it?

No. A component is a description of UI, so React must render the node tree. Use a server renderer or the temporary-DOM browser method depending on where conversion runs.

Will renderToStaticMarkup preserve React event handlers?

No. It returns non-interactive markup. Event handlers require a client bundle and hydration.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Is dangerouslySetInnerHTML required for every HTML string?

Only when trusted, sanitized markup must be interpreted as elements. For untrusted or ordinary text, render the value as a JSX child so React escapes it.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.