diff --git a/packages/vite-plugin-tanstack-start/test/e2e/build.test.ts b/packages/vite-plugin-tanstack-start/test/e2e/build.test.ts
index 06048bd26..26669ccc4 100644
--- a/packages/vite-plugin-tanstack-start/test/e2e/build.test.ts
+++ b/packages/vite-plugin-tanstack-start/test/e2e/build.test.ts
@@ -71,4 +71,34 @@ describe.runIf(isSupportedNode && !isWindows)('build output when deployed to Net
await page.click('text=sunt aut facere repe')
await page.waitForSelector('text=quia et suscipit suscipit')
})
+
+ test('Renders basic RSC page with server components', async () => {
+ const response = await page.goto(`${baseUrl}/rsc-basic`)
+ expect(response?.status()).toBe(200)
+ // The greeting is rendered by a server component via renderServerComponent
+ await page.waitForSelector('[data-testid="rsc-greeting"]')
+ const greetingText = await page.textContent('[data-testid="rsc-greeting"]')
+ expect(greetingText).toContain('Hello from a Server Component!')
+ // The user list is also rendered as a server component with data fetching
+ await page.waitForSelector('[data-testid="rsc-user-list"]')
+ const userListText = await page.textContent('[data-testid="rsc-user-list"]')
+ expect(userListText).toContain('Ervin Howell')
+ })
+
+ test('Renders composite RSC page with client component children', async () => {
+ const response = await page.goto(`${baseUrl}/rsc-composite`)
+ expect(response?.status()).toBe(200)
+ // The card shell is rendered on the server via createCompositeComponent
+ await page.waitForSelector('[data-testid="rsc-card"]')
+ const cardText = await page.textContent('[data-testid="rsc-card"]')
+ expect(cardText).toContain('Server-Rendered Card')
+ // The counter is a client component passed as a children slot
+ await page.waitForSelector('[data-testid="client-counter"]')
+ const initialValue = await page.textContent('[data-testid="counter-value"]')
+ expect(initialValue).toBe('0')
+ // Verify client interactivity works within the server-rendered shell
+ await page.click('[data-testid="counter-button"]')
+ const updatedValue = await page.textContent('[data-testid="counter-value"]')
+ expect(updatedValue).toBe('1')
+ })
})
diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/package.json b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/package.json
index 078da2d0c..9944e0118 100644
--- a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/package.json
+++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/package.json
@@ -10,9 +10,9 @@
"start": "node .output/server/index.mjs"
},
"dependencies": {
- "@tanstack/react-router": "^1.154.5",
- "@tanstack/react-router-devtools": "^1.154.5",
- "@tanstack/react-start": "^1.154.5",
+ "@tanstack/react-router": "^1.168.19",
+ "@tanstack/react-router-devtools": "^1.166.13",
+ "@tanstack/react-start": "^1.167.37",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^2.6.0",
@@ -21,6 +21,7 @@
"devDependencies": {
"@netlify/vite-plugin-tanstack-start": "file:../../..",
"@tailwindcss/vite": "^4.1.18",
+ "@vitejs/plugin-rsc": "^0.5.24",
"@types/node": "^22.5.4",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/components/Counter.tsx b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/components/Counter.tsx
new file mode 100644
index 000000000..9414aa38d
--- /dev/null
+++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/components/Counter.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react'
+
+/**
+ * A client-only interactive counter component.
+ * Used as a child slot inside a server-rendered composite component
+ * to demonstrate RSC composition with client interactivity.
+ */
+export function Counter() {
+ const [count, setCount] = React.useState(0)
+ return (
+
+
Client-side counter: {count}
+
setCount((c) => c + 1)}>
+ Increment
+
+
+ )
+}
diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routeTree.gen.ts b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routeTree.gen.ts
index 0bd698a66..abee081ae 100644
--- a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routeTree.gen.ts
+++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routeTree.gen.ts
@@ -12,6 +12,8 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as UsersRouteImport } from './routes/users'
import { Route as RedirectRouteImport } from './routes/redirect'
import { Route as PostsRouteImport } from './routes/posts'
+import { Route as RscBasicRouteImport } from './routes/rsc-basic'
+import { Route as RscCompositeRouteImport } from './routes/rsc-composite'
import { Route as DeferredRouteImport } from './routes/deferred'
import { Route as CustomScriptDotjsRouteImport } from './routes/customScript[.]js'
import { Route as PathlessLayoutRouteImport } from './routes/_pathlessLayout'
@@ -42,6 +44,16 @@ const PostsRoute = PostsRouteImport.update({
path: '/posts',
getParentRoute: () => rootRouteImport,
} as any)
+const RscBasicRoute = RscBasicRouteImport.update({
+ id: '/rsc-basic',
+ path: '/rsc-basic',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const RscCompositeRoute = RscCompositeRouteImport.update({
+ id: '/rsc-composite',
+ path: '/rsc-composite',
+ getParentRoute: () => rootRouteImport,
+} as any)
const DeferredRoute = DeferredRouteImport.update({
id: '/deferred',
path: '/deferred',
@@ -120,6 +132,8 @@ export interface FileRoutesByFullPath {
'/deferred': typeof DeferredRoute
'/posts': typeof PostsRouteWithChildren
'/redirect': typeof RedirectRoute
+ '/rsc-basic': typeof RscBasicRoute
+ '/rsc-composite': typeof RscCompositeRoute
'/users': typeof UsersRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
@@ -136,6 +150,8 @@ export interface FileRoutesByTo {
'/customScript.js': typeof CustomScriptDotjsRoute
'/deferred': typeof DeferredRoute
'/redirect': typeof RedirectRoute
+ '/rsc-basic': typeof RscBasicRoute
+ '/rsc-composite': typeof RscCompositeRoute
'/api/users': typeof ApiUsersRouteWithChildren
'/posts/$postId': typeof PostsPostIdRoute
'/users/$userId': typeof UsersUserIdRoute
@@ -154,6 +170,8 @@ export interface FileRoutesById {
'/deferred': typeof DeferredRoute
'/posts': typeof PostsRouteWithChildren
'/redirect': typeof RedirectRoute
+ '/rsc-basic': typeof RscBasicRoute
+ '/rsc-composite': typeof RscCompositeRoute
'/users': typeof UsersRouteWithChildren
'/_pathlessLayout/_nested-layout': typeof PathlessLayoutNestedLayoutRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
@@ -174,6 +192,8 @@ export interface FileRouteTypes {
| '/deferred'
| '/posts'
| '/redirect'
+ | '/rsc-basic'
+ | '/rsc-composite'
| '/users'
| '/api/users'
| '/posts/$postId'
@@ -190,6 +210,8 @@ export interface FileRouteTypes {
| '/customScript.js'
| '/deferred'
| '/redirect'
+ | '/rsc-basic'
+ | '/rsc-composite'
| '/api/users'
| '/posts/$postId'
| '/users/$userId'
@@ -207,6 +229,8 @@ export interface FileRouteTypes {
| '/deferred'
| '/posts'
| '/redirect'
+ | '/rsc-basic'
+ | '/rsc-composite'
| '/users'
| '/_pathlessLayout/_nested-layout'
| '/api/users'
@@ -227,6 +251,8 @@ export interface RootRouteChildren {
DeferredRoute: typeof DeferredRoute
PostsRoute: typeof PostsRouteWithChildren
RedirectRoute: typeof RedirectRoute
+ RscBasicRoute: typeof RscBasicRoute
+ RscCompositeRoute: typeof RscCompositeRoute
UsersRoute: typeof UsersRouteWithChildren
ApiUsersRoute: typeof ApiUsersRouteWithChildren
PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
@@ -262,6 +288,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DeferredRouteImport
parentRoute: typeof rootRouteImport
}
+ '/rsc-basic': {
+ id: '/rsc-basic'
+ path: '/rsc-basic'
+ fullPath: '/rsc-basic'
+ preLoaderRoute: typeof RscBasicRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/rsc-composite': {
+ id: '/rsc-composite'
+ path: '/rsc-composite'
+ fullPath: '/rsc-composite'
+ preLoaderRoute: typeof RscCompositeRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/customScript.js': {
id: '/customScript.js'
path: '/customScript.js'
@@ -429,6 +469,8 @@ const rootRouteChildren: RootRouteChildren = {
DeferredRoute: DeferredRoute,
PostsRoute: PostsRouteWithChildren,
RedirectRoute: RedirectRoute,
+ RscBasicRoute: RscBasicRoute,
+ RscCompositeRoute: RscCompositeRoute,
UsersRoute: UsersRouteWithChildren,
ApiUsersRoute: ApiUsersRouteWithChildren,
PostsPostIdDeepRoute: PostsPostIdDeepRoute,
diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/__root.tsx b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/__root.tsx
index 346409e9d..ded2f3ba6 100644
--- a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/__root.tsx
+++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/__root.tsx
@@ -111,6 +111,22 @@ function RootDocument({ children }: { children: React.ReactNode }) {
>
Deferred
{' '}
+
+ RSC Basic
+ {' '}
+
+ RSC Composite
+ {' '}
{
+ const [greeting, users] = await Promise.all([
+ fetchRscGreeting(),
+ fetchRscUserList(),
+ ])
+ return { ...greeting, ...users }
+ },
+ component: RscBasicPage,
+})
+
+function RscBasicPage() {
+ const { Greeting, UserList } = Route.useLoaderData()
+ return (
+
+
RSC Basic Demo
+
This page demonstrates basic React Server Components rendered via renderServerComponent.
+
+ {Greeting}
+
+ {UserList}
+
+ )
+}
diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/rsc-composite.tsx b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/rsc-composite.tsx
new file mode 100644
index 000000000..6d31c820c
--- /dev/null
+++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/routes/rsc-composite.tsx
@@ -0,0 +1,26 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { CompositeComponent } from '@tanstack/react-start/rsc'
+import { fetchRscCard } from '~/utils/rsc'
+import { Counter } from '~/components/Counter'
+
+export const Route = createFileRoute('/rsc-composite')({
+ loader: () => fetchRscCard(),
+ component: RscCompositePage,
+})
+
+function RscCompositePage() {
+ const { Card } = Route.useLoaderData()
+ return (
+
+
RSC Composite Demo
+
+ This page demonstrates createCompositeComponent with children slots.
+ The card shell is rendered on the server, while the counter inside is a client component.
+
+
+
+
+
+
+ )
+}
diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/utils/rsc.tsx b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/utils/rsc.tsx
new file mode 100644
index 000000000..1c39560a3
--- /dev/null
+++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/src/utils/rsc.tsx
@@ -0,0 +1,75 @@
+import * as React from 'react'
+import { createServerFn } from '@tanstack/react-start'
+import {
+ renderServerComponent,
+ createCompositeComponent,
+} from '@tanstack/react-start/rsc'
+
+/**
+ * A basic server function that renders a server component using renderServerComponent.
+ * The component fetches data on the server and renders it — the fetch logic never
+ * reaches the client bundle.
+ */
+export const fetchRscGreeting = createServerFn().handler(async () => {
+ // Simulate server-only work (e.g., DB query, secret API call)
+ const timestamp = new Date().toISOString()
+
+ const Greeting = await renderServerComponent(
+
+
Hello from a Server Component!
+
This was rendered on the server at {timestamp}.
+
+ This content comes from a React Server Component. The rendering logic
+ and any server-only dependencies never reach the client bundle.
+
+
,
+ )
+
+ return { Greeting }
+})
+
+/**
+ * A server function that fetches data and renders it as a server component.
+ * Demonstrates data fetching colocated within an RSC.
+ */
+export const fetchRscUserList = createServerFn().handler(async () => {
+ const res = await fetch('https://jsonplaceholder.typicode.com/users')
+ if (!res.ok) {
+ throw new Error('Failed to fetch users for RSC')
+ }
+ const users = (await res.json()) as Array<{ id: number; name: string; email: string }>
+
+ const UserList = await renderServerComponent(
+
+
Users (Server Component)
+
+ {users.slice(0, 5).map((user) => (
+
+ {user.name} — {user.email}
+
+ ))}
+
+
,
+ )
+
+ return { UserList }
+})
+
+/**
+ * A composite component that accepts children slots.
+ * The server renders the outer shell, and client components can be
+ * passed as children.
+ */
+export const fetchRscCard = createServerFn().handler(async () => {
+ const src = await createCompositeComponent(
+ (props: { children?: React.ReactNode }) => (
+
+
Server-Rendered Card
+
This card shell is rendered on the server.
+
{props.children}
+
+ ),
+ )
+
+ return { Card: src }
+})
diff --git a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/vite.config.ts b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/vite.config.ts
index 081d950c0..50160d26e 100644
--- a/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/vite.config.ts
+++ b/packages/vite-plugin-tanstack-start/test/fixtures/start-basic-rc/vite.config.ts
@@ -2,6 +2,7 @@ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import { defineConfig } from "vite";
import tsConfigPaths from "vite-tsconfig-paths";
import viteReact from "@vitejs/plugin-react";
+import rsc from "@vitejs/plugin-rsc";
import tailwindcss from "@tailwindcss/vite";
import netlify from "@netlify/vite-plugin-tanstack-start";
@@ -16,7 +17,9 @@ export default defineConfig({
}),
tanstackStart({
srcDirectory: "src",
+ rsc: { enabled: true },
}),
+ rsc(),
viteReact(),
netlify(),
],