Summary
When rsc: { enabled: true } is passed to the tanstackStart Vite plugin, createServerFn handlers are resolved with the react-server condition. This makes it impossible to use any library that depends on react-dom/server — including @react-email/render — inside a server function, even though the code only ever runs on the server.
Any import of react-dom/server (or react-dom/server.edge) resolves to react-dom/server.react-server.js, which simply throws:
react-dom/server is not supported in React Server Components.
By contrast, a Nitro/Elysia route handler defined in a server file route (e.g. routes/api.\$.ts with server.handlers) runs without the react-server condition and the same code works fine. This suggests the constraint is specific to how createServerFn is bundled when RSC is enabled, not a fundamental limitation.
Reproduction
Minimal repro: a createServerFn handler that dynamically imports @react-email/render and renders any React Email template.
```ts
// src/lib/nodemailer.ts
const sendEmail = async ({ react }: { react: JSX.Element }) => {
const { render } = await import('@react-email/render');
const html = await render(react); // throws in RSC server function
// ...
};
```
```ts
// src/routes/_auth/register.tsx
export const handleForm = createServerFn({ method: 'POST' })
.inputValidator(registerSchema)
.handler(async ({ data }) => {
// ...create user...
await sendEmail({ react: VerificationEmail({ name, url }) }); // ❌
});
```
```ts
// vite.config.ts
tanstackStart({
rsc: { enabled: true },
}),
rsc(),
```
What works
Putting the same render call inside an Elysia handler exposed via a server file route succeeds — same package, same component, same process:
```ts
// src/routes/api.$.ts
const app = new Elysia({ prefix: '/api' })
.get('/test-email-render', async () => {
const html = await render(VerificationEmail({ name: 'Test', url: 'https://example.com' }));
return { ok: true, length: html.length }; // ✅ { ok: true, length: 5690 }
});
```
What was tried
- Dynamic import of
@react-email/render — bundler still resolves it via react-server condition at request time.
- Aliasing
@react-email/render to its dist/edge/index.mjs (which uses react-dom/server.edge) — same error, because react-dom's own exports map maps react-server for both ./server and ./server.edge to the throwing stub.
- Pinning Vite
resolve.conditions to prefer workerd — doesn't reach into the RSC environment.
Expected behavior
createServerFn handlers should either:
- Run in a server environment that does not apply the
react-server condition (matching how server file route handlers work today), or
- Provide a documented way to opt a server function out of the RSC bundle when it needs
react-dom/server (e.g. for transactional email rendering, PDF generation, etc.).
Email rendering with @react-email/render is a common server-side need and currently has no workable path when RSC is enabled, other than introducing an internal HTTP hop to a Nitro/Elysia route handler.
Environment
- `@tanstack/react-start`: 1.168.14
- `@vitejs/plugin-rsc`: 0.5.26
- `react` / `react-dom`: 19.2.6
- `vite`: 8.0.14
- `@react-email/render`: 2.0.8
- Runtime: Bun (Nitro preset: `bun`)
- OS: macOS (Darwin 25.5.0)
Summary
When
rsc: { enabled: true }is passed to thetanstackStartVite plugin,createServerFnhandlers are resolved with thereact-servercondition. This makes it impossible to use any library that depends onreact-dom/server— including@react-email/render— inside a server function, even though the code only ever runs on the server.Any import of
react-dom/server(orreact-dom/server.edge) resolves toreact-dom/server.react-server.js, which simply throws:By contrast, a Nitro/Elysia route handler defined in a server file route (e.g.
routes/api.\$.tswithserver.handlers) runs without thereact-servercondition and the same code works fine. This suggests the constraint is specific to howcreateServerFnis bundled when RSC is enabled, not a fundamental limitation.Reproduction
Minimal repro: a
createServerFnhandler that dynamically imports@react-email/renderand renders any React Email template.```ts
// src/lib/nodemailer.ts
const sendEmail = async ({ react }: { react: JSX.Element }) => {
const { render } = await import('@react-email/render');
const html = await render(react); // throws in RSC server function
// ...
};
```
```ts
// src/routes/_auth/register.tsx
export const handleForm = createServerFn({ method: 'POST' })
.inputValidator(registerSchema)
.handler(async ({ data }) => {
// ...create user...
await sendEmail({ react: VerificationEmail({ name, url }) }); // ❌
});
```
```ts
// vite.config.ts
tanstackStart({
rsc: { enabled: true },
}),
rsc(),
```
What works
Putting the same render call inside an Elysia handler exposed via a server file route succeeds — same package, same component, same process:
```ts
// src/routes/api.$.ts
const app = new Elysia({ prefix: '/api' })
.get('/test-email-render', async () => {
const html = await render(VerificationEmail({ name: 'Test', url: 'https://example.com' }));
return { ok: true, length: html.length }; // ✅ { ok: true, length: 5690 }
});
```
What was tried
@react-email/render— bundler still resolves it viareact-servercondition at request time.@react-email/renderto itsdist/edge/index.mjs(which usesreact-dom/server.edge) — same error, becausereact-dom's ownexportsmap mapsreact-serverfor both./serverand./server.edgeto the throwing stub.resolve.conditionsto preferworkerd— doesn't reach into the RSC environment.Expected behavior
createServerFnhandlers should either:react-servercondition (matching how server file route handlers work today), orreact-dom/server(e.g. for transactional email rendering, PDF generation, etc.).Email rendering with
@react-email/renderis a common server-side need and currently has no workable path when RSC is enabled, other than introducing an internal HTTP hop to a Nitro/Elysia route handler.Environment