renderToStaticMarkup
Renders a React tree to static HTML string without React-specific attributes like data-reactroot.
import { renderToStaticMarkup } from 'react-dom/server' ;
const html = renderToStaticMarkup ( reactNode , options );
Use renderToStaticMarkup when you want to generate static HTML that won’t be hydrated by React on the client. This is ideal for email templates, static documentation, or non-interactive pages.
Reference
renderToStaticMarkup(reactNode, options?)
Call renderToStaticMarkup to render your React tree to static HTML.
/src/server/ReactDOMLegacyServerNode.js:30-40
const html = renderToStaticMarkup ( < Page /> );
Parameters
A React node you want to render to HTML. For example, a JSX element like <Page />.
Optional server rendering options. String prefix for IDs generated by useId. Typically not needed for static markup since there’s no hydration. const html = renderToStaticMarkup ( < App /> , {
identifierPrefix: 'static-'
});
Returns
A static HTML string without any React-specific attributes. Cannot be hydrated with hydrateRoot on the client.
Differences from renderToString
Feature renderToString renderToStaticMarkup React attributes Includes data-reactroot, etc. No React attributes Client hydration Can be hydrated Cannot be hydrated Output size Larger Smaller Use case SSR with hydration Static HTML only Event handlers Preserved for hydration Stripped
Output Comparison
import { renderToString , renderToStaticMarkup } from 'react-dom/server' ;
const component = < div className = "app" > Hello World </ div > ;
// renderToString output
renderToString ( component );
// <div class="app" data-reactroot="">Hello World</div>
// renderToStaticMarkup output
renderToStaticMarkup ( component );
// <div class="app">Hello World</div>
Usage Examples
Email Template Generation
import { renderToStaticMarkup } from 'react-dom/server' ;
function EmailTemplate ({ userName , orderNumber }) {
return (
< html >
< body style = { { fontFamily: 'Arial, sans-serif' } } >
< div style = { { maxWidth: '600px' , margin: '0 auto' } } >
< h1 > Order Confirmation </ h1 >
< p > Hi { userName } , </ p >
< p >
Your order < strong > # { orderNumber } </ strong > has been confirmed.
</ p >
< div style = { {
backgroundColor: '#f0f0f0' ,
padding: '20px' ,
borderRadius: '5px'
} } >
< h2 > Order Details </ h2 >
< p > Thank you for your purchase! </ p >
</ div >
</ div >
</ body >
</ html >
);
}
function sendOrderConfirmation ( user , order ) {
const emailHtml = renderToStaticMarkup (
< EmailTemplate userName = { user . name } orderNumber = { order . id } />
);
sendEmail ({
to: user . email ,
subject: 'Order Confirmation' ,
html: emailHtml ,
});
}
Static Site Generation
import { renderToStaticMarkup } from 'react-dom/server' ;
import fs from 'fs' ;
import path from 'path' ;
function DocumentationPage ({ title , content }) {
return (
< html lang = "en" >
< head >
< meta charSet = "utf-8" />
< title > { title } </ title >
< link rel = "stylesheet" href = "/styles.css" />
</ head >
< body >
< header >
< h1 > { title } </ h1 >
</ header >
< main dangerouslySetInnerHTML = { { __html: content } } />
< footer >
< p > © 2024 My Company </ p >
</ footer >
</ body >
</ html >
);
}
function generateStaticPages () {
const pages = [
{ slug: 'index' , title: 'Home' , content: '<p>Welcome!</p>' },
{ slug: 'about' , title: 'About' , content: '<p>About us</p>' },
];
pages . forEach (({ slug , title , content }) => {
const html = renderToStaticMarkup (
< DocumentationPage title = { title } content = { content } />
);
const filePath = path . join ( 'dist' , ` ${ slug } .html` );
fs . writeFileSync ( filePath , `<!DOCTYPE html> ${ html } ` );
});
}
generateStaticPages ();
import { renderToStaticMarkup } from 'react-dom/server' ;
function RSSFeed ({ items }) {
return (
< rss version = "2.0" >
< channel >
< title > My Blog </ title >
< link > https://example.com </ link >
< description > Latest posts </ description >
{ items . map ( item => (
< item key = { item . id } >
< title > { item . title } </ title >
< link > { item . url } </ link >
< description > { item . excerpt } </ description >
< pubDate > { item . date } </ pubDate >
</ item >
)) }
</ channel >
</ rss >
);
}
function generateRSSFeed ( posts ) {
const xml = renderToStaticMarkup ( < RSSFeed items = { posts } /> );
return `<?xml version="1.0" encoding="UTF-8"?> ${ xml } ` ;
}
PDF Report Generation
import { renderToStaticMarkup } from 'react-dom/server' ;
import puppeteer from 'puppeteer' ;
function Report ({ title , data }) {
return (
< html >
< head >
< style > { `
body { font-family: Arial; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; }
` } </ style >
</ head >
< body >
< h1 > { title } </ h1 >
< table >
< thead >
< tr >
< th > Name </ th >
< th > Value </ th >
</ tr >
</ thead >
< tbody >
{ data . map ( row => (
< tr key = { row . id } >
< td > { row . name } </ td >
< td > { row . value } </ td >
</ tr >
)) }
</ tbody >
</ table >
</ body >
</ html >
);
}
async function generatePDF ( title , data ) {
const html = renderToStaticMarkup (
< Report title = { title } data = { data } />
);
const browser = await puppeteer . launch ();
const page = await browser . newPage ();
await page . setContent ( `<!DOCTYPE html> ${ html } ` );
await page . pdf ({ path: 'report.pdf' , format: 'A4' });
await browser . close ();
}
SVG Generation
import { renderToStaticMarkup } from 'react-dom/server' ;
function Chart ({ data }) {
const width = 400 ;
const height = 200 ;
const maxValue = Math . max ( ... data . map ( d => d . value ));
return (
< svg width = { width } height = { height } xmlns = "http://www.w3.org/2000/svg" >
{ data . map (( item , i ) => {
const barHeight = ( item . value / maxValue ) * height ;
const x = i * ( width / data . length );
const y = height - barHeight ;
return (
< rect
key = { i }
x = { x }
y = { y }
width = { width / data . length - 10 }
height = { barHeight }
fill = "#3b82f6"
/>
);
}) }
</ svg >
);
}
function generateChartSVG ( data ) {
return renderToStaticMarkup ( < Chart data = { data } /> );
}
const svg = generateChartSVG ([
{ value: 10 },
{ value: 20 },
{ value: 15 },
]);
fs . writeFileSync ( 'chart.svg' , svg );
Use Cases
1. Email Marketing
Generate HTML emails with React components:
import { renderToStaticMarkup } from 'react-dom/server' ;
function NewsletterEmail ({ articles , subscriberName }) {
return (
< html >
< body style = { { backgroundColor: '#f4f4f4' , padding: '20px' } } >
< div style = { { maxWidth: '600px' , margin: '0 auto' , backgroundColor: 'white' } } >
< h1 > Hi { subscriberName } ! </ h1 >
< h2 > This Week's Top Articles </ h2 >
{ articles . map ( article => (
< div key = { article . id } style = { { padding: '15px' , borderBottom: '1px solid #eee' } } >
< h3 > { article . title } </ h3 >
< p > { article . excerpt } </ p >
< a href = { article . url } > Read more </ a >
</ div >
)) }
</ div >
</ body >
</ html >
);
}
2. Static Documentation
Generate documentation sites at build time:
import { renderToStaticMarkup } from 'react-dom/server' ;
import { marked } from 'marked' ;
function DocPage ({ markdown , navigation }) {
return (
< html >
< head >
< link rel = "stylesheet" href = "/docs.css" />
</ head >
< body >
< nav >
{ navigation . map ( item => (
< a key = { item . href } href = { item . href } > { item . label } </ a >
)) }
</ nav >
< main dangerouslySetInnerHTML = { { __html: marked ( markdown ) } } />
</ body >
</ html >
);
}
3. Open Graph Images
Generate social media preview images:
import { renderToStaticMarkup } from 'react-dom/server' ;
function OGImage ({ title , author }) {
return (
< svg width = "1200" height = "630" xmlns = "http://www.w3.org/2000/svg" >
< rect width = "1200" height = "630" fill = "#1e293b" />
< text
x = "600"
y = "300"
fontSize = "64"
fill = "white"
textAnchor = "middle"
fontFamily = "Arial"
>
{ title }
</ text >
< text
x = "600"
y = "400"
fontSize = "32"
fill = "#94a3b8"
textAnchor = "middle"
>
By { author }
</ text >
</ svg >
);
}
TypeScript
import { renderToStaticMarkup } from 'react-dom/server' ;
import type { ReactElement } from 'react' ;
interface ServerOptions {
identifierPrefix ?: string ;
}
function renderStaticPage ( component : ReactElement ) : string {
const html : string = renderToStaticMarkup ( component );
return `<!DOCTYPE html> ${ html } ` ;
}
const markup = renderStaticPage (< App />);
Smaller Output
import { renderToString , renderToStaticMarkup } from 'react-dom/server' ;
const component = < div > Hello </ div > ;
// renderToString: 43 bytes
const withReact = renderToString ( component );
// '<div data-reactroot="">Hello</div>'
// renderToStaticMarkup: 18 bytes
const withoutReact = renderToStaticMarkup ( component );
// '<div>Hello</div>'
// 58% size reduction!
Faster Rendering
import { renderToStaticMarkup } from 'react-dom/server' ;
const start = Date . now ();
const html = renderToStaticMarkup ( < LargeComponent /> );
const duration = Date . now () - start ;
console . log ( `Rendered in ${ duration } ms` );
// Typically 10-20% faster than renderToString
Limitations
No Hydration Support
HTML generated by renderToStaticMarkup cannot be hydrated with hydrateRoot. If you need interactivity, use renderToString or renderToPipeableStream instead.
// This won't work!
const html = renderToStaticMarkup ( < App /> );
// Send to client...
// Client-side
hydrateRoot ( container , < App /> ); // Hydration errors!
No Suspense Support
Same as renderToString, does not support Suspense boundaries:
import { Suspense } from 'react' ;
import { renderToStaticMarkup } from 'react-dom/server' ;
// This will throw an error
const html = renderToStaticMarkup (
< Suspense fallback = { < div > Loading... </ div > } >
< AsyncComponent />
</ Suspense >
);
Common Pitfalls
Don’t use for interactive content
// Bad: Button won't work without hydration
const html = renderToStaticMarkup (
< button onClick = { () => alert ( 'Hi' ) } > Click me </ button >
);
// Renders: <button>Click me</button>
// But onClick handler is stripped!
// Good: Use for truly static content
const html = renderToStaticMarkup (
< a href = "/contact" > Contact Us </ a >
);
Remember DOCTYPE for full HTML documents
import { renderToStaticMarkup } from 'react-dom/server' ;
// Don't forget DOCTYPE
const html = renderToStaticMarkup (
< html >
< body > Content </ body >
</ html >
);
const fullHtml = `<!DOCTYPE html> ${ html } ` ;
Testing with renderToStaticMarkup
import { renderToStaticMarkup } from 'react-dom/server' ;
import { expect , test } from 'vitest' ;
test ( 'Button renders correctly' , () => {
const html = renderToStaticMarkup (
< button className = "primary" > Click me </ button >
);
expect ( html ). toBe ( '<button class="primary">Click me</button>' );
});
test ( 'List renders all items' , () => {
const items = [ 'Apple' , 'Banana' , 'Cherry' ];
const html = renderToStaticMarkup (
< ul >
{ items . map ( item => < li key = { item } > { item } </ li > ) }
</ ul >
);
expect ( html ). toContain ( '<li>Apple</li>' );
expect ( html ). toContain ( '<li>Banana</li>' );
expect ( html ). toContain ( '<li>Cherry</li>' );
});
Best Practices
Use for truly static content - No need for client-side interactivity
Smaller HTML size - Perfect for emails and static pages
Include DOCTYPE - For complete HTML documents
Inline styles for emails - Email clients don’t support external CSS
Escape user content - React handles this automatically