ScreenshotNeo

BlogHow-to

How to Convert a React String to HTML

Learn which React API converts a component tree to HTML, how to render in Node or a browser, and how to safely insert existing HTML.

By the ScreenshotNeo team1 October 20267 min read

“Convert a React string to HTML” can mean several different operations. Choose the one that matches your input and output:

  • React element tree to an HTML string: use renderToStaticMarkup for non-interactive output.
  • React element tree to server HTML that will become interactive: use renderToString with client hydration, or a streaming API when progressive output is needed.
  • Render a React tree in a browser and read its HTML: mount it into a temporary DOM node, flush the update, then read innerHTML.
  • Insert an HTML string into React: use dangerouslySetInnerHTML only with trusted, sanitized content.

A React component is not itself a string. It is a description of a tree. The APIs below serialize or render that tree; they do not turn arbitrary JavaScript text into safe HTML.

1. Convert a React tree to static HTML

For emails, static pages, templates, and other non-hydratable output, use renderToStaticMarkup from react-dom/server. React documents this API as rendering a non-interactive React tree to an HTML string. The result cannot be hydrated.

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

function Invoice({ customer, total }) {
  return (
    <main>
      <h1>Invoice</h1>
      <p>Customer: {customer}</p>
      <p>Total: ${total}</p>
    </main>
  );
}

const html = renderToStaticMarkup(
  <Invoice customer="Ada Lovelace" total="42.00" />
);

console.log(html);

Use this when the consumer only needs markup. Because there is no React hydration data, event handlers and client state do not become active in the generated document. See the official renderToStaticMarkup reference.

Escaping values in static output

Values inserted as ordinary JSX children or attributes are escaped by React. Keep dynamic values in JSX rather than concatenating them into an HTML string.

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

const comment = '<script>alert("xss")</script>';
const html = renderToStaticMarkup(<p>{comment}</p>);

// The value is represented as text in the output, not executable markup.
console.log(html);

2. Convert a tree to HTML that will hydrate

If the HTML is the initial response for an interactive React application, use renderToString on the server and hydrate the matching tree in the browser.

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

const html = renderToString(<App />);
const documentHtml = `<!doctype html>
<html><head><title>App</title></head>
<body><div id="root">${html}</div><script type="module" src="/client.js"></script></body></html>`;

// Send documentHtml from your HTTP framework.
// client.js
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import { App } from './App.js';

hydrateRoot(document.getElementById('root'), <App />);

renderToString returns immediately. It does not stream output or wait for suspended data, and a suspended child renders its nearest Suspense fallback. If those constraints matter, use a streaming or prerender API described in React’s server rendering documentation.

Streaming and prerendering choices

Requirement API Constraint
Static HTML string renderToStaticMarkup Cannot hydrate
Immediate server HTML plus hydration renderToString + hydrateRoot No streaming or data waiting
Progressive Node.js response renderToPipeableStream Node stream runtime
Progressive Web Streams response renderToReadableStream Web Streams runtime
Wait for data before returning static output prerender and its runtime counterpart Designed to wait rather than progressively stream

3. Render React in a browser and read innerHTML

React does not recommend importing server rendering code into client bundles just to serialize a component. Instead, create a temporary DOM element, render into it with createRoot, synchronously flush the update, and read innerHTML.

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

function Card() {
  return <article className="card"><h2>Hello</h2></article>;
}

export function reactTreeToHtml() {
  const container = document.createElement('div');
  document.body.appendChild(container);
  const root = createRoot(container);

  flushSync(() => {
    root.render(<Card />);
  });

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

console.log(reactTreeToHtml());

flushSync makes the DOM update available before you read it. This method produces the browser’s current DOM serialization, including browser normalization. It is useful when the component depends on browser APIs, but it incurs browser rendering work and should not be used as a server-side replacement.

4. If you already have an HTML string

If your input is already HTML and your goal is to insert it into a React component, use dangerouslySetInnerHTML only after the content has been trusted and sanitized.

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

React warns that untrusted HTML, including user-provided content, can introduce an XSS vulnerability. A string containing <strong>hello</strong> is not automatically safe merely because it came from a database or API. Sanitize it at a trusted boundary, restrict allowed elements and attributes, and consider a TrustedHTML value when Trusted Types are enforced. Read React’s common components guidance.

If the content should be displayed as text, use ordinary children instead:

function PlainText({ value }) {
  return <p>{value}</p>;
}

5. Common mistakes and fixes

Symptom Cause Fix
“Objects are not valid as a React child” A component object or arbitrary object was passed where text or an element was expected. Render a React element, or explicitly choose the fields to display.
The result is not interactive renderToStaticMarkup creates non-hydratable HTML. Use renderToString plus hydrateRoot, or a streaming API.
Hydration mismatch warnings Server and client trees differ because of time, randomness, locale, data, or conditional browser code. Make the first render deterministic and use the same props on both sides.
Suspense content is missing from the initial string renderToString does not wait for suspended data. Use a streaming or prerender API appropriate to your runtime.
HTML tags appear as text The string was rendered as a normal child, so React escaped it. Keep it as text for safety, or use sanitized dangerouslySetInnerHTML when markup is required.
XSS after inserting a string Untrusted HTML was passed to dangerouslySetInnerHTML. Remove the injection path, sanitize at the boundary, and allow only the markup you need.
Empty browser result The DOM was read before React committed the render. Use flushSync for this synchronous conversion, then unmount and remove the temporary node.
Server bundle error involving window or document Browser-only code ran during server rendering. Move browser access into effects or client-only modules and keep the server tree deterministic.

6. Performance, reliability, and output details

  • Choose the smallest API: static output avoids hydration data; hydration adds client work; browser serialization adds DOM and commit work.
  • Reuse the server process: import React and your components once rather than rebuilding the module graph for every conversion.
  • Keep server renders deterministic: pass data explicitly and avoid reading changing clocks, random values, or client-only state during the first render.
  • Use streaming for large interactive responses: it can send available HTML progressively when your runtime supports the streaming server APIs.
  • Clean up browser conversions: unmount the temporary root and remove its container to avoid retained DOM and subscriptions.
  • Validate the consumer: an email client, HTTP response, template engine, and browser may apply different parsing rules to the same string.
  • Measure serialization separately from network time: server rendering cost depends on tree size, component work, data preparation, and whether a browser is involved.

7. Or skip the browser setup

If your end goal is a visual capture of the rendered page rather than an HTML string, ScreenshotNeo returns a screenshot or PDF from one GET request. It can accept a live URL after your React page is deployed, so you do not need to install or operate a browser for the capture.

See the ScreenshotNeo API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Sign up for the free ScreenshotNeo plan.

8. FAQ

Can I convert a JSX string such as "<Button />" directly?

No. JSX syntax in a JavaScript string is only text until a compiler or parser turns it into a component tree. Prefer importing and rendering the component itself. Parsing arbitrary JSX also raises code execution and trust concerns.

Does renderToStaticMarkup include event handlers?

It emits static markup. React event behavior is not attached, and the output cannot be hydrated.

Should I use innerHTML or outerHTML?

Use innerHTML when you need the children of a temporary wrapper. Use outerHTML only when the wrapper element itself belongs in the result.

Which API is best for an email?

Use renderToStaticMarkup, then apply the email client’s required CSS and sanitization rules. Email clients do not provide React hydration.

Can React sanitize an existing HTML string for me?

No. React escapes ordinary children, but dangerouslySetInnerHTML assumes you handled trust and sanitization before insertion.

Where can I verify the API behavior?

Use the official references for static rendering, string rendering and hydration, server APIs, and HTML insertion security.