UNPKG

@rsc-kit/mcp

Version:

An MCP server over what an rsc-kit build decided: the routes, why each one is static or not, and what it costs the browser.

103 lines (76 loc) • 3.95 kB
# Emails and other HTML > Rendering React to HTML on the server — an email, a PDF, a feed — from a server action or a route, with "use ssr". An email template is a React component, and `@react-email/render` (or `renderToString` from `react-dom/server`) turns it into HTML. Call that from a server action and this comes back: ``` Error: react-dom/server is not supported in React Server Components. ``` React means it. Where server components render, `react` is a server-only build: the renderer needs the client build's internals, and the components it would render import that same `react` — no `useState`, no `useContext`. Nothing that renders React to HTML can run there, and no alias fixes that. It has to run in the **ssr** environment, which every app here already has: the one that turns your pages into HTML for the browser. ## `"use ssr"` Put the rendering — the template and the call that renders it — in a module that starts with the directive: ```tsx title="src/lib/email/render.tsx" "use ssr"; import { render } from '@react-email/render'; import { OtpEmail } from './otp-email'; export async function renderOtpEmail(code: string) { const email = <OtpEmail code={code} />; const [html, text] = await Promise.all([ render(email), render(email, { plainText: true }), ]); return { html, text }; } ``` Everything else imports it normally: ```ts title="src/lib/email/send-otp.ts" import { renderOtpEmail } from './render'; export async function sendOtpEmail(to: string, code: string) { const { html, text } = await renderOtpEmail(code); await transporter.sendMail({ to, subject: 'Your code', html, text }); } ``` The module runs in the ssr environment, in the same process. Where server components render, the build replaces it with proxies of its exports that call across — the same thing `"use client"` does for a component, in the other direction. In development that is a call into the ssr module runner; in a build the module is part of the ssr bundle and the proxies import it from there. Nothing to configure. ## The rules **Exports are async functions.** A call crosses environments, so the answer is a promise. A function declared without `async` is refused at build with its name; so is a value, a class, `export { … }` or `export *`. Types are fine. **Pass data across, not elements.** The template is created inside the module, from the arguments — `renderOtpEmail(code)`, not `render(<OtpEmail />)` from the caller. An element built where server components render carries components from that side's `react`, and they would render with no hooks. **The module's imports belong to the ssr side.** `@react-email/components`, `react-dom/server`, a PDF renderer, a Markdown library that uses React — all resolved and bundled for the ssr environment. A database client works there too, but belongs on the calling side; keep the module to rendering. ## If you import it directly anyway `react-dom/server` imported where server components render — through a library, usually — now fails with the fix in the message rather than React's refusal: ``` react-dom/server cannot run where server components render … Imported by src/lib/nodemailer.ts. Put the rendering — the template and the call — in a module that starts with "use ssr" … ``` A build prints the same once, as a warning, naming the file — and the build still succeeds, because the refusal is the call's, not the import's: every export of the stub throws it when called, so a server does not fail to boot over an action nobody has run yet. A dependency whose imports the build never looks inside — `@react-email/render` imported from an action, say — is read once for the import and warned about the same way, naming the app file and the package: ``` src/actions/send-otp.ts imports @react-email/render, which imports react-dom/server, and that cannot run where server components render … ```