renderToStaticMarkup
renderToStaticMarkup renders a React tree to static HTML without React-specific attributes. This is useful for generating static pages or email templates where client-side hydration is not needed.
const html = renderToStaticMarkup ( element , options ? );
Unlike renderToString, this produces clean HTML without data-reactroot or other React attributes. The resulting HTML cannot be hydrated on the client.
Reference
renderToStaticMarkup(element, options?)
Renders a React element to a static HTML string without React attributes.
import { renderToStaticMarkup } from 'react-dom/server' ;
const html = renderToStaticMarkup ( < Page /> );
Parameters
element : ReactNode - The React element to render
options : Object (optional)
identifierPrefix : string - Prefix for IDs generated by useId
Returns
Returns a string containing clean HTML without React-specific attributes.
Caveats
No hydration : Output cannot be hydrated with hydrateRoot on the client
No Suspense : Does not support Suspense boundaries
Blocking : Synchronous rendering blocks until complete
No interactivity : Event handlers and client-side state won’t work
Usage
Static site generation
Use renderToStaticMarkup to generate static HTML files at build time:
import { renderToStaticMarkup } from 'react-dom/server' ;
import fs from 'fs' ;
import { BlogPost } from './BlogPost' ;
// Generate static HTML for a blog post
function generateStaticPage ( post ) {
const html = renderToStaticMarkup (
< html >
< head >
< meta charSet = "utf-8" />
< title > { post . title } </ title >
< link rel = "stylesheet" href = "/styles.css" />
</ head >
< body >
< BlogPost { ... post } />
</ body >
</ html >
);
fs . writeFileSync ( `dist/ ${ post . slug } .html` , '<!DOCTYPE html>' + html );
}
// Generate pages for all posts
posts . forEach ( generateStaticPage );
Email templates
Generate HTML emails using React components:
import { renderToStaticMarkup } from 'react-dom/server' ;
function WelcomeEmail ({ userName , verificationLink }) {
return (
< html >
< body style = { { fontFamily: 'Arial, sans-serif' } } >
< h1 > Welcome, { userName } ! </ h1 >
< p > Thank you for signing up. Please verify your email: </ p >
< a
href = { verificationLink }
style = { {
display: 'inline-block' ,
padding: '10px 20px' ,
backgroundColor: '#007bff' ,
color: 'white' ,
textDecoration: 'none'
} }
>
Verify Email
</ a >
</ body >
</ html >
);
}
const emailHtml = renderToStaticMarkup (
< WelcomeEmail
userName = "Alice"
verificationLink = "https://example.com/verify?token=abc123"
/>
);
// Send email with emailHtml
await sendEmail ({
to: 'alice@example.com' ,
subject: 'Welcome!' ,
html: emailHtml
});
Documentation generation
Generate static documentation pages:
Build Script
Doc Component
import { renderToStaticMarkup } from 'react-dom/server' ;
import { mkdir , writeFile } from 'fs/promises' ;
import { DocPage } from './components/DocPage' ;
import { docs } from './docs-data' ;
async function buildDocs () {
await mkdir ( 'dist/docs' , { recursive: true });
for ( const doc of docs ) {
const html = '<!DOCTYPE html>' + renderToStaticMarkup (
< DocPage
title = { doc . title }
content = { doc . content }
sidebar = { docs . map ( d => ({
title: d . title ,
href: `/ ${ d . slug } .html`
})) }
/>
);
await writeFile ( `dist/docs/ ${ doc . slug } .html` , html );
}
console . log ( `Built ${ docs . length } documentation pages` );
}
buildDocs ();
export function DocPage ({ title , content , sidebar }) {
return (
< html lang = "en" >
< head >
< meta charSet = "utf-8" />
< meta name = "viewport" content = "width=device-width, initial-scale=1" />
< title > { title } - Documentation </ title >
< link rel = "stylesheet" href = "/docs.css" />
</ head >
< body >
< div className = "layout" >
< aside className = "sidebar" >
< nav >
< ul >
{ sidebar . map ( item => (
< li key = { item . href } >
< a href = { item . href } > { item . title } </ a >
</ li >
)) }
</ ul >
</ nav >
</ aside >
< main >
< h1 > { title } </ h1 >
< div dangerouslySetInnerHTML = { { __html: content } } />
</ main >
</ div >
</ body >
</ html >
);
}
Generate RSS XML using React components:
import { renderToStaticMarkup } from 'react-dom/server' ;
function RSSFeed ({ title , description , items }) {
return (
< rss version = "2.0" >
< channel >
< title > { title } </ title >
< description > { description } </ description >
< link > https://example.com </ link >
{ items . map ( item => (
< item key = { item . id } >
< title > { item . title } </ title >
< link > { item . url } </ link >
< description > { item . description } </ description >
< pubDate > {new Date ( item . date ). toUTCString () } </ pubDate >
</ item >
)) }
</ channel >
</ rss >
);
}
const rss = '<?xml version="1.0" encoding="UTF-8"?>' +
renderToStaticMarkup (
< RSSFeed
title = "My Blog"
description = "Latest posts"
items = { blogPosts }
/>
);
fs . writeFileSync ( 'dist/feed.xml' , rss );
Differences from renderToString
Output comparison
renderToStaticMarkup
renderToString
import { renderToStaticMarkup } from 'react-dom/server' ;
const html = renderToStaticMarkup (
< div className = "greeting" >
< h1 > Hello, World! </ h1 >
</ div >
);
console . log ( html );
// Output (clean HTML):
// <div class="greeting"><h1>Hello, World!</h1></div>
import { renderToString } from 'react-dom/server' ;
const html = renderToString (
< div className = "greeting" >
< h1 > Hello, World! </ h1 >
</ div >
);
console . log ( html );
// Output (with React attributes):
// <div class="greeting" data-reactroot="">
// <h1>Hello, World!</h1>
// </div>
When to use each
renderToStaticMarkup Use when:
Generating static sites
Creating email templates
No client-side interactivity needed
Cleaner HTML desired
Building time content
renderToString Use when:
Server-side rendering with hydration
Client-side interactivity needed
React will run on the client
Event handlers required
Dynamic updates expected
Common Patterns
Building a static site generator
import { renderToStaticMarkup } from 'react-dom/server' ;
import { glob } from 'glob' ;
import { mkdir , writeFile } from 'fs/promises' ;
import path from 'path' ;
class StaticSiteGenerator {
constructor ( options ) {
this . pages = options . pages ;
this . outDir = options . outDir ;
this . layout = options . layout ;
}
async build () {
await mkdir ( this . outDir , { recursive: true });
for ( const page of this . pages ) {
const content = await this . renderPage ( page );
const filePath = path . join ( this . outDir , page . path );
await mkdir ( path . dirname ( filePath ), { recursive: true });
await writeFile ( filePath , content );
console . log ( `✓ Built ${ page . path } ` );
}
}
async renderPage ( page ) {
const Layout = this . layout ;
const Page = page . component ;
const html = renderToStaticMarkup (
< Layout { ... page . layoutProps } >
< Page { ... page . props } />
</ Layout >
);
return `<!DOCTYPE html> ${ html } ` ;
}
}
// Usage
const generator = new StaticSiteGenerator ({
outDir: 'dist' ,
layout: Layout ,
pages: [
{ path: 'index.html' , component: HomePage , props: {} },
{ path: 'about.html' , component: AboutPage , props: {} },
{ path: 'blog/post-1.html' , component: BlogPost , props: { id: 1 } },
]
});
await generator . build ();
Template rendering utility
import { renderToStaticMarkup } from 'react-dom/server' ;
class TemplateRenderer {
static render ( Component , props = {}) {
return renderToStaticMarkup ( < Component { ... props } /> );
}
static renderWithDoctype ( Component , props = {}) {
return '<!DOCTYPE html>' + this . render ( Component , props );
}
static renderToFile ( Component , props , filePath ) {
const html = this . renderWithDoctype ( Component , props );
return fs . promises . writeFile ( filePath , html );
}
}
// Usage
const html = TemplateRenderer . render ( EmailTemplate , { name: 'Alice' });
await TemplateRenderer . renderToFile ( BlogPost , { id: 1 }, 'dist/post.html' );
Memory usage
Like renderToString, renderToStaticMarkup buffers the entire output in memory:
// For very large pages, this can use significant memory
const largeHtml = renderToStaticMarkup ( < VeryLargePage /> );
// Consider breaking into smaller chunks
const sections = [
renderToStaticMarkup ( < Header /> ),
renderToStaticMarkup ( < Content /> ),
renderToStaticMarkup ( < Footer /> )
];
const html = sections . join ( '' );
import { renderToStaticMarkup } from 'react-dom/server' ;
import pLimit from 'p-limit' ;
// Limit concurrent renders to avoid overwhelming memory
const limit = pLimit ( 10 );
const renderPromises = pages . map ( page =>
limit ( async () => {
const html = renderToStaticMarkup ( < Page { ... page } /> );
await fs . promises . writeFile ( page . output , html );
})
);
await Promise . all ( renderPromises );
Runtime Availability
renderToStaticMarkup is available across all runtimes:
import { renderToStaticMarkup } from 'react-dom/server' ;
// or
import { renderToStaticMarkup } from 'react-dom/server.node' ;
import { renderToStaticMarkup } from 'react-dom/server.edge' ;
import { renderToStaticMarkup } from 'react-dom/server.bun' ;
Common Issues
Event handlers don't work
renderToStaticMarkup produces static HTML without React. Event handlers won’t be attached:// This onClick won't work - HTML is static
const html = renderToStaticMarkup (
< button onClick = { () => alert ( 'Hi' ) } > Click me </ button >
);
// Output: <button>Click me</button>
// No onClick handler in the HTML!
Solution : Use renderToString with client-side hydration if you need interactivity.
Cannot hydrate static markup
Attempting to hydrate markup from renderToStaticMarkup will fail: // Server
const html = renderToStaticMarkup ( < App /> );
// Client - This will error!
hydrateRoot ( document . getElementById ( 'root' ), < App /> );
// Error: Expected server HTML to contain matching elements
Solution : Use renderToString on the server if you need client-side hydration.
Like renderToString, renderToStaticMarkup doesn’t support Suspense: // This will throw an error
renderToStaticMarkup (
< Suspense fallback = { < div > Loading... </ div > } >
< AsyncComponent />
</ Suspense >
);
Solution : Ensure all data is loaded before rendering, or use streaming APIs.
See Also