RenderComponent

Use renderComponent() to server-render React components outside the page router — for emails, PDFs, API responses, scheduled jobs, or any context where you need HTML without a full HTTP request/response cycle.

Core Feature

Overview

renderComponent() is a server-only function that renders any React component to an HTML string with full SSR support — hooks, async components, layouts, and all. Unlike the page router (which ties rendering to HTTP routes), renderComponent()runs standalone: call it from API routes, cron jobs, CLI scripts, or anywhere you need server-rendered markup programmatically.

ℹ️
Import from 'nukejs/server'renderComponent is a server-only API. Import it from 'nukejs/server', not 'nukejs', to avoid pulling Node.js built-ins into client bundles.

Basic usage

scripts/generate-email.tstypescript
import { renderComponent } from 'nukejs/server'
import WelcomeEmail from '../app/emails/Welcome'

const html = await renderComponent(WelcomeEmail, {
    name: 'Alice',
    activationLink: 'https://example.com/activate?token=xyz'
})

console.log(html) // Full HTML document string

Signature

renderComponent(
    Component: React.ComponentType<any>,
    props?: Record<string, any>,
    options?: RenderComponentOptions
): Promise<string>

Options

OptionTypeDescription
layoutsReact.ComponentType[]Array of layout components to wrap the main component. Applied innermost-first.
urlstringURL pathname for the request context (default: '/'). Used by useRequest().
paramsRecord<string, string | string[]>Route params exposed via useRequest().params
queryRecord<string, string | string[]>Query string params exposed via useRequest().query
headersRecord<string, string>Request headers exposed via useRequest().headers
titlestringDefault document title (default: 'NukeJS')
isDevbooleanDevelopment mode flag. Auto-detected from NODE_ENV if not set.

Example: Email templates

Render transactional email HTML using React components. Share styles and layout components across all your emails.

app/emails/Welcome.tsxtypescript
import { useHtml } from 'nukejs'

export default function WelcomeEmail({ name, activationLink }: {
    name: string
    activationLink: string
}) {
    useHtml({ title: 'Welcome!' })

    return (
        <div style={{ fontFamily: 'sans-serif', padding: '20px' }}>
            <h1>Welcome, {name}!</h1>
            <p>Thank you for signing up. Please activate your account:</p>
            <a
                href={activationLink}
                style={{
                    display: 'inline-block',
                    padding: '10px 20px',
                    background: '#0070f3',
                    color: 'white',
                    textDecoration: 'none',
                    borderRadius: '4px'
                }}
            >
                Activate Account
            </a>
            <p style={{ marginTop: '20px', fontSize: '12px', color: '#666' }}>
                If you didn't sign up, ignore this email.
            </p>
        </div>
    )
}
lib/email.tstypescript
import { renderComponent } from 'nukejs/server'
import nodemailer from 'nodemailer'
import WelcomeEmail from '../app/emails/Welcome'

const transporter = nodemailer.createTransport({
    host: process.env.SMTP_HOST,
    port: 587,
    auth: {
        user: process.env.SMTP_USER,
        pass: process.env.SMTP_PASS,
    },
})

export async function sendWelcomeEmail(to: string, name: string, token: string) {
    const activationLink = `https://example.com/activate?token=${token}`

    const html = await renderComponent(WelcomeEmail, {
        name,
        activationLink
    })

    await transporter.sendMail({
        from: 'noreply@example.com',
        to,
        subject: 'Welcome to Our App',
        html,
    })
}

Example: PDF generation

Combine renderComponent() with a headless browser to generate PDFs from React components:

app/templates/Invoice.tsxtypescript
export default function InvoiceTemplate({ invoice }: { invoice: any }) {
    return (
        <div style={{ padding: '40px', fontFamily: 'Arial, sans-serif' }}>
            <h1>Invoice #{invoice.number}</h1>
            <p>Date: {new Date(invoice.date).toLocaleDateString()}</p>
            <hr />
            <h2>Items</h2>
            <table style={{ width: '100%', borderCollapse: 'collapse' }}>
                <thead>
                    <tr style={{ borderBottom: '2px solid #000' }}>
                        <th style={{ textAlign: 'left' }}>Description</th>
                        <th style={{ textAlign: 'right' }}>Amount</th>
                    </tr>
                </thead>
                <tbody>
                    {invoice.items.map((item: any, i: number) => (
                        <tr key={i}>
                            <td>{item.description}</td>
                            <td style={{ textAlign: 'right' }}>${item.amount.toFixed(2)}</td>
                        </tr>
                    ))}
                </tbody>
            </table>
            <hr />
            <p style={{ textAlign: 'right', fontSize: '18px', fontWeight: 'bold' }}>
                Total: ${invoice.total.toFixed(2)}
            </p>
        </div>
    )
}
lib/pdf.tstypescript
import { renderComponent } from 'nukejs/server'
import puppeteer from 'puppeteer'
import InvoiceTemplate from '../app/templates/Invoice'

export async function generateInvoicePDF(invoice: any): Promise<Buffer> {
    // Render component to HTML
    const html = await renderComponent(InvoiceTemplate, { invoice })

    // Launch headless browser
    const browser = await puppeteer.launch()
    const page = await browser.newPage()
    await page.setContent(html, { waitUntil: 'networkidle0' })

    // Generate PDF
    const pdf = await page.pdf({
        format: 'A4',
        printBackground: true,
    })

    await browser.close()
    return pdf
}

Example: API route returning HTML

Render dynamic HTML responses in API routes without going through the page router:

server/preview/[id].tstypescript
import type { IncomingMessage, ServerResponse } from 'http'
import { renderComponent } from 'nukejs/server'
import BlogPost from '../../app/components/BlogPost'
import { getPostById } from '../../lib/db'

export async function GET(
    req: IncomingMessage & { params: any },
    res: ServerResponse
) {
    const postId = parseInt(req.params.id as string)
    const post = await getPostById(postId)

    if (!post) {
        res.writeHead(404, { 'Content-Type': 'text/plain' })
        res.end('Not found')
        return
    }

    // Render the component with custom headers
    const html = await renderComponent(BlogPost,
        { post },
        {
            url: `/preview/${postId}`,
            headers: { 'x-preview': 'true' },
            title: post.title,
        }
    )

    res.writeHead(200, { 'Content-Type': 'text/html' })
    res.end(html)
}

Using layouts

Wrap your component in one or more layout components to share structure, styles, or context providers:

app/layouts/EmailLayout.tsxtypescript
export default function EmailLayout({ children }: { children: React.ReactNode }) {
    return (
        <html>
            <head>
                <meta charSet="utf-8" />
                <style>{`
                    body {
                        margin: 0;
                        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
                    }
                `}</style>
            </head>
            <body>
                <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
                    {children}
                </div>
            </body>
        </html>
    )
}
scripts/send-newsletter.tstypescript
import { renderComponent } from 'nukejs/server'
import EmailLayout from '../app/layouts/EmailLayout'
import Newsletter from '../app/emails/Newsletter'

const html = await renderComponent(
    Newsletter,
    { articles: latestArticles },
    {
        layouts: [EmailLayout],  // Wraps Newsletter in EmailLayout
        title: 'Weekly Newsletter'
    }
)

// html is a complete <!DOCTYPE html> document with the layout applied

Request context with useRequest()

Components rendered via renderComponent() can use useRequest()to access the synthetic request context you provide:

app/components/Report.tsxtypescript
import { useRequest } from 'nukejs'

export default function Report() {
    const { query, headers } = useRequest()

    const format = query.format as string ?? 'summary'  // From options.query
    const locale = headers['accept-language'] ?? 'en'   // From options.headers

    return (
        <div>
            <p>Report format: {format}</p>
            <p>Locale: {locale}</p>
        </div>
    )
}
scripts/generate-report.tstypescript
import { renderComponent } from 'nukejs/server'
import Report from '../app/components/Report'

const html = await renderComponent(Report, {}, {
    url: '/reports/monthly',
    query: { format: 'detailed' },
    headers: { 'accept-language': 'fr-FR' },
})

// The Report component sees query.format === 'detailed'
// and headers['accept-language'] === 'fr-FR'

When to use renderComponent()

Use caseWhy
Email templatesReuse React components for transactional and marketing emails
PDF generationRender invoices, reports, certificates as HTML → PDF
API routes returning HTMLPreview endpoints, embeds, or dynamic snippets
Static site generationPre-render pages in a build script
Scheduled jobsGenerate and send HTML summaries on a cron schedule
TestingRender components in tests to verify output
⚠️
No client-side hydration HTML generated by renderComponent() is pure SSR output. It does not include the hydration script or client bundle. If you need interactive components, use the page router instead.

TypeScript

lib/render.tstypescript
import { renderComponent } from 'nukejs/server'
import type { RenderComponentOptions } from 'nukejs/server'

// Type-safe options
const options: RenderComponentOptions = {
    url: '/foo',
    query: { bar: 'baz' },
    headers: { 'x-custom': 'value' },
}

const html = await renderComponent(MyComponent, { prop: 'value' }, options)