Skip to main content

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:
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();

RSS feed generation

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

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>

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');

Performance Considerations

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('');

Build performance

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';

Common Issues

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.
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