Skip to main content

renderToString

renderToString synchronously renders a React tree to an HTML string. This is a legacy API that does not support Suspense or streaming.
const html = renderToString(element, options?);
renderToString does not support Suspense and cannot stream output. For new projects, use renderToPipeableStream (Node.js) or renderToReadableStream (Web Streams) instead.

Reference

renderToString(element, options?)

Renders a React element to an HTML string with React-specific attributes like data-reactroot.
import { renderToString } from 'react-dom/server';

const html = renderToString(<App />);

Parameters

  • element: ReactNode - The React element to render
  • options: Object (optional)
    • identifierPrefix: string - Prefix for IDs generated by useId. Useful for avoiding conflicts when using multiple roots on the same page.

Returns

Returns a string containing the rendered HTML with React attributes.

Caveats

  • No Suspense support: If a component suspends, renderToString will throw an error
  • Blocking: Must wait for the entire tree to render before returning
  • No streaming: Cannot send HTML to client until everything is ready
  • Client-side: Also available in browser environments but not commonly used there

Usage

Basic server-side rendering

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

function handleRequest(req, res) {
  const html = renderToString(<App />);
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>My App</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `);
}

Using with Express

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

const app = express();

app.get('/', (req, res) => {
  try {
    const html = renderToString(<App />);
    
    res.send(`
      <!DOCTYPE html>
      <html>
        <head>
          <meta charset="utf-8" />
          <title>My App</title>
          <link rel="stylesheet" href="/styles.css" />
        </head>
        <body>
          <div id="root">${html}</div>
          <script src="/bundle.js"></script>
        </body>
      </html>
    `);
  } catch (error) {
    console.error('Rendering error:', error);
    res.status(500).send('Internal Server Error');
  }
});

app.listen(3000);

Setting identifier prefix

Use identifierPrefix to avoid ID conflicts when rendering multiple React roots on the same page:
import { renderToString } from 'react-dom/server';

function ComponentWithId() {
  const id = useId();
  return <label htmlFor={id}>Name: <input id={id} /></label>;
}

// Render multiple independent apps
const html1 = renderToString(<App1 />, {
  identifierPrefix: 'app1-'
});

const html2 = renderToString(<App2 />, {
  identifierPrefix: 'app2-'
});

// IDs won't conflict:
// app1-:r0: vs app2-:r0:

Handling errors

renderToString throws errors synchronously, so use try-catch:
import { renderToString } from 'react-dom/server';
import ErrorPage from './ErrorPage';

function renderApp() {
  try {
    return renderToString(<App />);
  } catch (error) {
    console.error('Failed to render:', error);
    // Return error page HTML
    return renderToString(<ErrorPage error={error.message} />);
  }
}

Limitations

No Suspense Support

If your component tree contains a Suspense boundary with a component that suspends, renderToString will throw an error:
// This will throw an error
function App() {
  return (
    <Suspense fallback={<Loading />}>
      <AsyncComponent /> {/* If this suspends, renderToString throws */}
    </Suspense>
  );
}

renderToString(<App />); // Error!
Solution: Use renderToPipeableStream or renderToReadableStream which fully support Suspense.

Blocking Rendering

renderToString must render the entire component tree before returning:
function App() {
  // Even if this takes 5 seconds, renderToString waits
  const data = fetchDataSync(); // Blocks!
  return <div>{data}</div>;
}

// User sees nothing until ALL rendering completes
const html = renderToString(<App />);
Solution: Use streaming APIs to send HTML progressively as components become ready.

Performance Considerations

renderToString is synchronous and blocks the event loop:
app.get('/', (req, res) => {
  // This blocks the server for ALL users during rendering
  const html = renderToString(<LargeApp />); // 100ms
  res.send(html);
  
  // No other requests can be processed during these 100ms
});
Solution: Use streaming APIs which are non-blocking and allow concurrent request handling.

Migrating to Modern APIs

If you’re using renderToString, consider migrating to modern streaming APIs:
import { renderToString } from 'react-dom/server';

app.get('/', (req, res) => {
  const html = renderToString(<App />);
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <body>
        <div id="root">${html}</div>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `);
});

Runtime Availability

renderToString is available in multiple runtime exports:
import { renderToString } from 'react-dom/server';
// or
import { renderToString } from 'react-dom/server.node';
Uses ReactDOMLegacyServerNode implementation.

Common Issues

renderToString cannot handle Suspense boundaries. Remove Suspense or migrate to renderToPipeableStream/renderToReadableStream.
// Remove this:
<Suspense fallback={<div>Loading...</div>}>
  <AsyncComponent />
</Suspense>

// Or migrate to streaming API
Ensure server and client render identical HTML. Common causes:
  • Using Date.now() or Math.random() during render
  • Different data on server vs client
  • Browser-specific APIs used during render
// Bad: Different on server and client
<div>{Date.now()}</div>

// Good: Use useEffect for client-only code
function Clock() {
  const [time, setTime] = useState(null);
  useEffect(() => {
    setTime(Date.now());
  }, []);
  return <div>{time || 'Loading...'}</div>;
}
renderToString builds the entire HTML string in memory. For very large pages, this can cause memory issues.Solution: Use streaming APIs which write directly to the response stream without buffering.

See Also