renderToString
Renders a React tree to an HTML string synchronously on the server.
import { renderToString } from 'react-dom/server' ;
const html = renderToString ( reactNode , options );
Reference
renderToString(reactNode, options?)
Call renderToString to render your app to HTML on the server.
/src/server/ReactDOMLegacyServerNode.js:18-28
const html = renderToString ( < App /> );
Parameters
A React node you want to render to HTML. For example, a JSX element like <App />.
Optional server rendering options. String prefix React uses for IDs generated by useId. Useful to avoid conflicts when using multiple roots on the same page. const html1 = renderToString ( < App /> , { identifierPrefix: 'app1-' });
const html2 = renderToString ( < App /> , { identifierPrefix: 'app2-' });
Returns
An HTML string with React-specific attributes like data-reactroot. This HTML can be sent to the client and hydrated with hydrateRoot.
Usage Examples
Basic Server 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="/client.js"></script>
</body>
</html>
` );
}
With Client-Side Hydration
Server:
import { renderToString } from 'react-dom/server' ;
import App from './App' ;
import express from 'express' ;
const app = express ();
app . get ( '*' , ( req , res ) => {
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="/client.js"></script>
</body>
</html>
` );
});
app . listen ( 3000 );
Client:
import { hydrateRoot } from 'react-dom/client' ;
import App from './App' ;
hydrateRoot ( document . getElementById ( 'root' ), < App /> );
With Express and Template
import { renderToString } from 'react-dom/server' ;
import express from 'express' ;
import App from './App' ;
const app = express ();
const template = ( html ) => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>React SSR App</title>
<link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
<div id="root"> ${ html } </div>
<script src="/static/js/main.js"></script>
</body>
</html>
` ;
app . get ( '*' , ( req , res ) => {
try {
const html = renderToString ( < App /> );
res . send ( template ( html ));
} catch ( error ) {
console . error ( 'SSR Error:' , error );
res . status ( 500 ). send ( 'Internal Server Error' );
}
});
app . listen ( 3000 );
With Next.js (Legacy)
// pages/index.js
import { renderToString } from 'react-dom/server' ;
export default function Home ({ html }) {
return < div dangerouslySetInnerHTML = { { __html: html } } /> ;
}
export async function getServerSideProps () {
const html = renderToString ( < MyComponent /> );
return {
props: { html },
};
}
Limitations
Does Not Support Suspense
renderToString does not support Suspense. Components that suspend will cause an error.
import { Suspense } from 'react' ;
import { renderToString } from 'react-dom/server' ;
// This will throw an error
try {
const html = renderToString (
< Suspense fallback = { < div > Loading... </ div > } >
< AsyncComponent />
</ Suspense >
);
} catch ( error ) {
console . error ( 'Suspense not supported in renderToString' );
}
Solution: Use renderToPipeableStream instead:
import { renderToPipeableStream } from 'react-dom/server' ;
const { pipe } = renderToPipeableStream (
< Suspense fallback = { < div > Loading... </ div > } >
< AsyncComponent />
</ Suspense > ,
{
onShellReady () {
pipe ( res );
},
}
);
Synchronous and Blocking
renderToString is synchronous and blocks the Node.js event loop until rendering completes. This can cause performance issues for large apps.
import { renderToString } from 'react-dom/server' ;
// Blocks the event loop - not ideal for production
const html = renderToString ( < LargeApp /> );
Solution: Use streaming APIs:
import { renderToPipeableStream } from 'react-dom/server' ;
// Streams HTML as it's generated - better performance
const { pipe } = renderToPipeableStream ( < LargeApp /> , {
onShellReady () {
pipe ( res );
},
});
No Streaming
Client must wait for entire HTML string before anything renders:
// Bad: Client waits for complete HTML
const html = renderToString ( < App /> ); // 2 seconds
res . send ( html ); // Now client can start parsing
// Good: Client receives HTML incrementally
const { pipe } = renderToPipeableStream ( < App /> , {
onShellReady () {
pipe ( res ); // Start sending HTML immediately
},
});
Migration Guide
From renderToString to renderToPipeableStream
Before:
import { renderToString } from 'react-dom/server' ;
import express from 'express' ;
const app = express ();
app . get ( '*' , ( req , res ) => {
const html = renderToString ( < App /> );
res . send ( `
<!DOCTYPE html>
<html>
<body>
<div id="root"> ${ html } </div>
<script src="/client.js"></script>
</body>
</html>
` );
});
After:
import { renderToPipeableStream } from 'react-dom/server' ;
import express from 'express' ;
const app = express ();
app . get ( '*' , ( req , res ) => {
res . setHeader ( 'content-type' , 'text/html' );
const { pipe } = renderToPipeableStream (
< html >
< body >
< div id = "root" >
< App />
</ div >
< script src = "/client.js" ></ script >
</ body >
</ html > ,
{
onShellReady () {
pipe ( res );
},
onError ( error ) {
console . error ( 'SSR Error:' , error );
res . statusCode = 500 ;
},
}
);
});
Benefits of Migration
Suspense Support - Async components work properly
Streaming - HTML sent to client as it’s generated
Better Performance - Non-blocking, scales better
Selective Hydration - Interactive sooner on client
Error Handling - Better error recovery
When to Use renderToString
Despite being legacy, renderToString is still useful for:
Static Site Generation - Pre-rendering at build time
Email Templates - Generating HTML emails
Testing - Simple HTML output for snapshots
Simple Apps - No Suspense, small scale
Static Site Generation Example
import { renderToString } from 'react-dom/server' ;
import fs from 'fs' ;
import path from 'path' ;
function buildStaticSite () {
const pages = [
{ path: '/' , component: < Home /> },
{ path: '/about' , component: < About /> },
{ path: '/contact' , component: < Contact /> },
];
pages . forEach (({ path : pagePath , component }) => {
const html = renderToString ( component );
const fullHtml = `
<!DOCTYPE html>
<html>
<head><title>My Site</title></head>
<body>
<div id="root"> ${ html } </div>
</body>
</html>
` ;
const filePath = path . join ( 'dist' , pagePath , 'index.html' );
fs . mkdirSync ( path . dirname ( filePath ), { recursive: true });
fs . writeFileSync ( filePath , fullHtml );
});
}
buildStaticSite ();
TypeScript
import { renderToString } from 'react-dom/server' ;
import type { ReactElement } from 'react' ;
interface ServerOptions {
identifierPrefix ?: string ;
}
function renderPage ( component : ReactElement ) : string {
const html : string = renderToString ( component , {
identifierPrefix: 'app-' ,
});
return `
<!DOCTYPE html>
<html>
<body>
<div id="root"> ${ html } </div>
</body>
</html>
` ;
}
const markup = renderPage (< App />);
Error Handling
import { renderToString } from 'react-dom/server' ;
function renderWithErrorHandling ( component ) {
try {
const html = renderToString ( component );
return { success: true , html };
} catch ( error ) {
console . error ( 'Server rendering error:' , error );
// Render error page
const errorHtml = renderToString ( < ErrorPage error = { error } /> );
return { success: false , html: errorHtml };
}
}
const result = renderWithErrorHandling ( < App /> );
if ( result . success ) {
res . send ( result . html );
} else {
res . status ( 500 ). send ( result . html );
}
Measure Rendering Time
import { renderToString } from 'react-dom/server' ;
function measureRenderTime ( component ) {
const start = Date . now ();
const html = renderToString ( component );
const duration = Date . now () - start ;
console . log ( `SSR took ${ duration } ms` );
return html ;
}
Optimize Large Apps
import { renderToString } from 'react-dom/server' ;
import { cache } from './cache' ;
function renderWithCache ( component , cacheKey ) {
// Check cache first
const cached = cache . get ( cacheKey );
if ( cached ) return cached ;
// Render and cache
const html = renderToString ( component );
cache . set ( cacheKey , html , { ttl: 60 * 1000 }); // 1 minute
return html ;
}
Common Pitfalls
Don’t use browser APIs
// Bad: window not available on server
function Component () {
const width = window . innerWidth ; // Error!
return < div > Width: { width } </ div > ;
}
// Good: Check environment
function Component () {
const width = typeof window !== 'undefined' ? window . innerWidth : 0 ;
return < div > Width: { width } </ div > ;
}
Don’t forget to escape user content
import { renderToString } from 'react-dom/server' ;
import he from 'he' ;
// Bad: XSS vulnerability
function renderPage ( userContent ) {
return `
<div id="content"> ${ userContent } </div>
` ;
}
// Good: React escapes by default
function renderPage ( userContent ) {
const html = renderToString (
< div id = "content" > { userContent } </ div >
);
return html ;
}