Skip to content

RSC: createServerFn handlers cannot use react-dom/server (blocks @react-email/render, etc.) #7500

@ramonmalcolm10

Description

@ramonmalcolm10

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:

  1. Run in a server environment that does not apply the react-server condition (matching how server file route handlers work today), or
  2. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions