TanStack / TanStack/router

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

Open
#7,500 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

information needed
Dominant language
TypeScript
Stars
15.1k
Forks
1.9k
Avg merge
1d 20h
Merged PRs (30d)
143

Description

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)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the minimal createServerFn reproduction in src/lib/nodemailer.ts and the RSC-enabled tanstackStart configuration in vite.config.ts, then compare it with the working handler in src/routes/api.$.ts. Reproduce the react-server resolution failure and trace how the two handler types are bundled; done means a server function can render with @react-email/render without the throwing react-dom/server stub.

Written by the indexing model from the issue text.

Assessment

Tech stack
bun, react, typescript, vite
Domain
backend, backend-api-design, build-system
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.