Documentation Index Fetch the complete documentation index at: https://mintlify.com/facebook/react/llms.txt
Use this file to discover all available pages before exploring further.
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 ? );
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 );
import { renderToString } from 'react-dom/server.edge' ;
import App from './App' ;
export default async function handler ( request ) {
try {
const html = renderToString ( < App /> );
return new Response ( `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>My App</title>
</head>
<body>
<div id="root"> ${ html } </div>
<script src="/bundle.js"></script>
</body>
</html>
` , {
headers: { 'Content-Type' : 'text/html' }
});
} catch ( error ) {
return new Response ( 'Internal Server Error' , { status: 500 });
}
}
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.
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>
` );
});
import { renderToPipeableStream } from 'react-dom/server' ;
app . get ( '/' , ( req , res ) => {
const { pipe } = renderToPipeableStream ( < App /> , {
bootstrapScripts: [ '/bundle.js' ],
onShellReady () {
res . setHeader ( 'Content-Type' , 'text/html' );
pipe ( res );
},
onError ( error ) {
console . error ( error );
}
});
});
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. import { renderToString } from 'react-dom/server.edge' ;
Uses ReactDOMLegacyServerBrowser implementation. import { renderToString } from 'react-dom/server.browser' ;
Available but rarely used in browser contexts.
Common Issues
Suspense error: does not support Suspense
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
Hydration mismatch warnings
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 > ;
}
Memory leaks with large renders
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