@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
Markdown
# 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 `-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.** `-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 — `-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 -email/render, which imports
react-dom/server, and that cannot run where server components render …
```