Skip to main content

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

reactNode
ReactNode
required
A React node you want to render to HTML. For example, a JSX element like <Page />.
options
ServerOptions
Optional server rendering options.

Returns

html
string
A static HTML string without any React-specific attributes. Cannot be hydrated with hydrateRoot on the client.

Differences from renderToString

FeaturerenderToStringrenderToStaticMarkup
React attributesIncludes data-reactroot, etc.No React attributes
Client hydrationCan be hydratedCannot be hydrated
Output sizeLargerSmaller
Use caseSSR with hydrationStatic HTML only
Event handlersPreserved for hydrationStripped

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>&copy; 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();

RSS Feed Generation

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

Performance Benefits

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

  1. Use for truly static content - No need for client-side interactivity
  2. Smaller HTML size - Perfect for emails and static pages
  3. Include DOCTYPE - For complete HTML documents
  4. Inline styles for emails - Email clients don’t support external CSS
  5. Escape user content - React handles this automatically