服务器组件(Server Components)
警告
服务器组件是实验性功能!API 将来可能会有所调整。
服务器组件(Server Components)让你在服务器上渲染 React 组件,并把它们流式传输给客户端。重依赖不进打包产物、数据获取就在组件里、敏感逻辑永远不会到达浏览器。
安装配置
服务器组件默认不开启。 先完成下面三步:
1. 安装构建工具专属的 RSC 依赖
npm install -D @vitejs/plugin-rsc
# or
pnpm add -D @vitejs/plugin-rsc
# or
yarn add -D @vitejs/plugin-rsc
# or
bun add -D @vitejs/plugin-rsc2. 配置你的构建工具
更新构建工具配置,在 TanStack Start 插件中启用 RSC:
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import rsc from '@vitejs/plugin-rsc'
export default defineConfig({
plugins: [
tanstackStart({
rsc: {
enabled: true,
},
}),
rsc(),
viteReact(),
],
})版本要求: React 19+,Vite 7+ 或 Rsbuild 2+
快速上手
在 TanStack Start 中,你通常在一个服务器函数里创建服务端渲染的 UI,然后通过路由的 loader 返回它。
有两个高层的 RSC 辅助函数:
renderServerComponent(<Element />)返回一个可渲染的值(renderable value),可以像{Renderable}一样内联使用。createCompositeComponent((props) => <Element />)返回一个复合源(composite source),通过<CompositeComponent src={...} />渲染(支持插槽 slots)。
Renderable(无插槽)
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { renderServerComponent } from '@tanstack/react-start/rsc'
function Greeting() {
return <h1>Hello from RSC</h1>
}
const getGreeting = createServerFn().handler(async () => {
const Renderable = await renderServerComponent(<Greeting />)
return { Renderable }
})
export const Route = createFileRoute('/')({
loader: async () => {
const { Renderable } = await getGreeting()
return { Greeting: Renderable }
},
component: HomePage,
})
function HomePage() {
const { Greeting } = Route.useLoaderData()
return <>{Greeting}</>
}Composite(带插槽)
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import {
CompositeComponent,
createCompositeComponent,
} from '@tanstack/react-start/rsc'
const getCard = createServerFn().handler(async () => {
const src = await createCompositeComponent(
(props: { children?: React.ReactNode }) => (
<div className="card">
<h2>Server-rendered header</h2>
<div>{props.children}</div>
</div>
),
)
return { src }
})
export const Route = createFileRoute('/')({
loader: async () => ({
Card: await getCard(),
}),
component: HomePage,
})
function HomePage() {
const { Card } = Route.useLoaderData()
return (
<CompositeComponent src={Card.src}>
<Counter />
</CompositeComponent>
)
}译者注:什么是「客户端主导组合」?
这是 Start 的 RSC 思路与 Next.js 不同的地方:在 Start 里,服务器组件的输出被当作「数据」交给客户端——客户端负责组合(在 <CompositeComponent> 里填 children、render props 等插槽),服务器只负责交付 UI 片段。所以叫「客户端主导组合(client-led composition)」。你无需学习一套新的「组件缓存」语义,缓存、流式、插槽都按你熟悉的模式来。
为什么用服务器组件?
- 更小的打包体积。 Markdown 解析器、语法高亮器、重型库都在服务器上运行。只有渲染好的 HTML 会到达客户端。
- 就近取数(Colocated data fetching)。 直接在你需要数据的组件里获取数据。
- 默认安全。 API 密钥、数据库查询、业务逻辑永远不会出现在客户端打包产物中。
- 渐进式流式传输。 UI 随着渲染逐步流向浏览器。用户能立刻看到内容。
传递 Props 与组合
renderServerComponent 返回的 renderable 值不支持插槽。
要接收客户端提供的 props(「插槽」),使用 createCompositeComponent 并通过 <CompositeComponent src={...} /> 渲染。
插槽在服务器组件的 props 上声明。有三种插槽类型:
| 插槽类型 | 用途 | 服务器能否传数据? |
|---|---|---|
children | 简单的组合 | 否 |
| Render props | 服务器向客户端渲染的内容传数据 | 是 |
| 组件 props | 传入接收服务器数据的可复用组件 | 是 |
Children 插槽
把客户端组件作为 children 传入。简单、熟悉,但服务器无法向它们传数据:
import {
CompositeComponent,
createCompositeComponent,
} from '@tanstack/react-start/rsc'
const getCard = createServerFn().handler(async () => {
const src = await createCompositeComponent(
(props: { children?: React.ReactNode }) => (
<div className="card">
<h2>Server-rendered header</h2>
<div>{props.children}</div>
</div>
),
)
return { src }
})
function MyPage() {
const { src } = Route.useLoaderData()
return (
<CompositeComponent src={src}>
{/* Client components with full interactivity */}
<Counter />
<button onClick={() => alert('Clicked!')}>Click me</button>
</CompositeComponent>
)
}Render Props
当服务器需要向客户端渲染的内容传数据时,使用 render props:
import {
CompositeComponent,
createCompositeComponent,
} from '@tanstack/react-start/rsc'
const getPost = createServerFn()
.validator(z.object({ postId: z.string() }))
.handler(async ({ data }) => {
const post = await db.posts.findById(data.postId)
const src = await createCompositeComponent(
(props: {
children?: React.ReactNode
renderActions?: (data: {
postId: string
authorId: string
}) => React.ReactNode
}) => (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<footer>
{props.renderActions?.({
postId: post.id,
authorId: post.authorId,
})}
</footer>
{props.children}
</article>
),
)
return { src }
})
function PostPage() {
const { src } = Route.useLoaderData()
return (
<CompositeComponent
src={src}
renderActions={({ postId, authorId }) => (
<PostActions postId={postId} authorId={authorId} />
)}
>
<Comments />
</CompositeComponent>
)
}组件 Props
把 React 组件作为 props 传入。在客户端,传入的 props 会用服务器提供的数据渲染:
import {
CompositeComponent,
createCompositeComponent,
} from '@tanstack/react-start/rsc'
const getProductCard = createServerFn()
.validator(z.object({ productId: z.string() }))
.handler(async ({ data }) => {
const product = await db.products.findById(data.productId)
const src = await createCompositeComponent(
({
AddToCart,
}: {
AddToCart: React.ComponentType<{ productId: string; price: number }>
}) => (
<div className="product-card">
<h2>{product.name}</h2>
<p>${product.price}</p>
<AddToCart productId={product.id} price={product.price} />
</div>
),
)
return { src }
})
// Client component with interactivity
function AddToCartButton({
productId,
price,
}: {
productId: string
price: number
}) {
const [added, setAdded] = React.useState(false)
return (
<button onClick={() => setAdded(true)}>
{added ? '✓ Added!' : `Add to Cart - $${price}`}
</button>
)
}
function ProductPage() {
const { src } = Route.useLoaderData()
return <CompositeComponent src={src} AddToCart={AddToCartButton} />
}组件 props 在以下场景很有用:
- 你想传入可复用的客户端组件
- 组件需要自己的 state 或事件处理器
- 相比 render prop 回调,你更偏好组件组合
三种插槽类型可以组合使用。createCompositeComponent 为插槽 props 提供完整的类型安全。
缓存
服务器组件与 TanStack Router 的内置缓存配合工作。缓存 key 是路由路径加上参数:
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => ({
Post: await getPost({ data: { postId: params.postId } }),
}),
component: PostPage,
})导航到 /posts/abc,再到 /posts/xyz,然后返回 /posts/abc——缓存的组件会瞬间渲染。
用 staleTime 控制新鲜度:
export const Route = createFileRoute('/posts/$postId')({
staleTime: 10_000, // Fresh for 10 seconds
loader: async ({ params }) => ({
Post: await getPost({ data: { postId: params.postId } }),
}),
component: PostPage,
})当缓存 key 需要超出路由参数时,使用 loaderDeps:
export const Route = createFileRoute('/posts/$postId')({
loaderDeps: ({ search }) => ({ tab: search.tab }),
loader: async ({ params, deps }) => ({
Post: await getPost({ data: { postId: params.postId, tab: deps.tab } }),
}),
component: PostPage,
})TanStack Query
要获得更细粒度的控制,使用 TanStack Query:
import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query'
const postQueryOptions = (postId: string) => ({
queryKey: ['post', postId],
structuralSharing: false, // Required - RSC values must not be merged
queryFn: () => getPost({ data: { postId } }),
staleTime: 5 * 60 * 1000,
})
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ context, params }) => {
// Prefetch during SSR - data reused on client without refetch
await context.queryClient.ensureQueryData(postQueryOptions(params.postId))
},
component: PostPage,
})
function PostPage() {
const { postId } = Route.useParams()
const queryClient = useQueryClient()
const { data } = useSuspenseQuery(postQueryOptions(postId))
const handleRefresh = () => {
// Manually refetch the RSC
queryClient.refetchQueries({ queryKey: ['post', postId] })
}
return <CompositeComponent src={data.src} />
}重要
用 React Query 缓存服务器组件时,一定要设置 structuralSharing: false。否则 React Query 可能尝试在多次 fetch 之间合并 RSC 值,这会导致错误。
失效(Invalidation)
数据变更后要重新获取服务器组件,使用 router.invalidate():
import { useRouter } from '@tanstack/react-router'
function PostPage() {
const router = useRouter()
const { Post } = Route.useLoaderData()
const handleUpdate = async () => {
await updatePost({ data: { ... } })
// Refetch the route's loader, including the RSC
router.invalidate()
}
return (
<CompositeComponent
src={Post.src}
renderActions={() => (
<button onClick={handleUpdate}>Update Post</button>
)}
/>
)
}当服务器函数修改了 RSC 展示的数据时,这个模式很有用。变更完成后,router.invalidate() 会触发加载器重新运行,获取包含最新数据的服务器组件。
与选择性 SSR 组合
当你需要服务端渲染的内容、但路由组件本身要仅客户端渲染时,服务器组件可以与选择性 SSR搭配使用。
示例:ssr: 'data-only'
服务器获取 RSC,但路由组件在客户端渲染:
export const Route = createFileRoute('/dashboard')({
ssr: 'data-only',
loader: async () => ({
Dashboard: await getDashboard(),
}),
component: DashboardPage,
})
function DashboardPage() {
const { Dashboard } = Route.useLoaderData()
const [width, setWidth] = React.useState(0)
React.useEffect(() => {
setWidth(window.innerWidth) // Browser API
}, [])
return (
<Dashboard
renderChart={({ data }) => <ResponsiveChart data={data} width={width} />}
/>
)
}这在以下场景很有用:
- 服务器组件获取数据并渲染静态内容
- 路由组件需要
window、localStorage或其他浏览器 API - 你想在客户端渲染之前让服务器组件数据就绪
示例:ssr: false
加载器和组件都在客户端运行:
export const Route = createFileRoute('/canvas')({
ssr: false,
loader: async () => {
const savedState = localStorage.getItem('canvas-state')
return { Tools: await getDrawingTools({ data: { savedState } }) }
},
component: CanvasPage,
})当加载器本身就需要浏览器 API 时,使用这种方式。
高级模式
并行获取多个服务器组件
当一个页面需要几个独立的服务器组件时,用 Promise.all 并行获取它们。每个组件用各自的数据独立渲染。
什么时候用: 组件来自相互独立的数据源,没有共享依赖。当每个组件有独立的获取逻辑时,能最大化并发度。
const getArticleA = createServerFn().handler(async () => {
const article = await db.articles.findById('a')
return renderServerComponent(<Article data={article} />)
})
const getArticleB = createServerFn().handler(async () => {
const article = await db.articles.findById('b')
return renderServerComponent(<Article data={article} />)
})
const getSidebar = createServerFn().handler(async () => {
const trending = await db.articles.getTrending()
return renderServerComponent(<Sidebar items={trending} />)
})
export const Route = createFileRoute('/news')({
loader: async () => {
const [ArticleA, ArticleB, Sidebar] = await Promise.all([
getArticleA(),
getArticleB(),
getSidebar(),
])
return { ArticleA, ArticleB, Sidebar }
},
component: NewsPage,
})
function NewsPage() {
const { ArticleA, ArticleB, Sidebar } = Route.useLoaderData()
return (
<div className="grid">
<main>
{ArticleA}
{ArticleB}
</main>
<aside>{Sidebar}</aside>
</div>
)
}每个服务器函数并发执行。全部完成后页面才渲染。
打包多个组件
当多个服务器组件共享数据、或应该一起获取时,可以从一个服务器函数中返回它们。这样能减少网络往返次数。
什么时候用: 组件共享获取的数据、需要同一个缓存 key,或应该一起失效。能减少数据库查询和网络往返。
使用 Promise.all
创建多次 renderServerComponent 或 createCompositeComponent 调用,并把它们作为一个对象返回:
const getPageLayout = createServerFn().handler(async () => {
// Fetch shared data once
const user = await db.users.getCurrent()
const config = await db.config.get()
// Create multiple components that share this data
const [Header, Content, Footer] = await Promise.all([
renderServerComponent(
<header>
<Logo />
<nav>
{config.navItems.map((item) => (
<NavLink key={item.id} {...item} />
))}
</nav>
<UserMenu name={user.name} />
</header>,
),
renderServerComponent(
<main>
<h1>Welcome, {user.name}</h1>
<Dashboard stats={user.stats} />
</main>,
),
renderServerComponent(
<footer>
<span>{config.copyright}</span>
{config.footerLinks.map((link) => (
<a key={link.id} href={link.url}>
{link.label}
</a>
))}
</footer>,
),
])
return { Header, Content, Footer }
})
export const Route = createFileRoute('/dashboard')({
loader: async () => await getPageLayout(),
component: DashboardPage,
})
function DashboardPage() {
const { Header, Content, Footer } = Route.useLoaderData()
return (
<>
{Header}
{Content}
{Footer}
</>
)
}使用嵌套结构
或者,从一个服务器函数返回嵌套的对象结构。
- 当你只需要 renderable(无插槽)时,用
renderServerComponent。 - 当你想让每个部分都接受插槽时,用
createCompositeComponent。
const getPageLayout = createServerFn().handler(async () => {
const user = await db.users.getCurrent()
const config = await db.config.get()
const [Header, Content, Footer] = await Promise.all([
createCompositeComponent((props: { children?: React.ReactNode }) => (
<header>
<Logo />
<nav>
{config.navItems.map((item) => (
<NavLink key={item.id} {...item} />
))}
</nav>
<UserMenu name={user.name} />
{props.children}
</header>
)),
createCompositeComponent(
(props: { renderActions?: () => React.ReactNode }) => (
<main>
<h1>Welcome, {user.name}</h1>
<Dashboard stats={user.stats} />
{props.renderActions?.()}
</main>
),
),
createCompositeComponent(() => (
<footer>
<span>{config.copyright}</span>
{config.footerLinks.map((link) => (
<a key={link.id} href={link.url}>
{link.label}
</a>
))}
</footer>
)),
])
return { Header, Content, Footer }
})
export const Route = createFileRoute('/dashboard')({
loader: async () => ({
Layout: await getPageLayout(),
}),
component: DashboardPage,
})用点号记法渲染嵌套的复合组件:
import { CompositeComponent } from '@tanstack/react-start/rsc'
function DashboardPage() {
const { Layout } = Route.useLoaderData()
return (
<>
<CompositeComponent src={Layout.Header}>
<button onClick={() => setMenuOpen(true)}>Menu</button>
</CompositeComponent>
<CompositeComponent
src={Layout.Content}
renderActions={() => <ActionButtons />}
/>
<CompositeComponent src={Layout.Footer} />
</>
)
}或者从加载器数据中解构它们:
import { CompositeComponent } from '@tanstack/react-start/rsc'
function DashboardPage() {
const { Header, Content, Footer } = Route.useLoaderData().Layout
return (
<>
<CompositeComponent src={Header}>
<button onClick={() => setMenuOpen(true)}>Menu</button>
</CompositeComponent>
<CompositeComponent
src={Content}
renderActions={() => <ActionButtons />}
/>
<CompositeComponent src={Footer} />
</>
)
}每个嵌套组件独立接收自己的插槽 props。传给 <CompositeComponent src={Header}> 的 children 只影响那个组件,不影响其他组件。
三个组件共享来自同一次数据库查询的 user 和 config 数据。
延迟组件加载
不 await 服务器组件的 Promise,直接返回它们。客户端用 React.use() 配合 Suspense,让每个组件在解析完成后渲染。
什么时候用: 组件的数据延迟不同,希望更快的结果先渲染,而不是被最慢的查询卡住。避免等待最慢的查询。
import { Suspense, use } from 'react'
const getDashboardBundle = createServerFn().handler(() => ({
// Fast - resolves in ~100ms
QuickStats: (async () => {
const stats = await cache.getStats() // Fast cache hit
return renderServerComponent(<StatsCard data={stats} />)
})(),
// Medium - resolves in ~500ms
RecentActivity: (async () => {
const activity = await db.activity.getRecent()
return renderServerComponent(<ActivityFeed items={activity} />)
})(),
// Slow - resolves in ~2000ms
Analytics: (async () => {
const data = await analytics.computeMetrics() // Expensive query
return renderServerComponent(<AnalyticsChart data={data} />)
})(),
}))
export const Route = createFileRoute('/dashboard')({
loader: () => getDashboardBundle(),
component: DashboardPage,
})
function DashboardPage() {
const { QuickStats, RecentActivity, Analytics } = Route.useLoaderData()
return (
<div>
<Suspense fallback={<Skeleton />}>
<Deferred promise={QuickStats} />
</Suspense>
<Suspense fallback={<Skeleton />}>
<Deferred promise={RecentActivity} />
</Suspense>
<Suspense fallback={<Skeleton />}>
<Deferred promise={Analytics} />
</Suspense>
</div>
)
}
function Deferred({ promise }: { promise: Promise<unknown> }) {
const Renderable = use(promise)
return <>{Renderable}</>
}QuickStats 最先出现,RecentActivity 随后,Analytics 最后加载。用户看到的是渐进呈现的内容,而不是等待所有东西。
服务器组件内部的 Suspense
直接在服务器组件内部使用 React 的 Suspense,让组件的各部分在就绪后流式呈现。
什么时候用: 单个服务器组件里有多个异步子组件,希望它们独立流式呈现。把相关的 UI 保留在一个组件里,同时允许渐进式渲染。
async function SlowMetric({ label, delay }: { label: string; delay: number }) {
await new Promise((resolve) => setTimeout(resolve, delay))
const value = await db.metrics.get(label)
return (
<div className="metric">
<span>{label}</span>
<span>{value.toLocaleString()}</span>
</div>
)
}
const getAnalyticsDashboard = createServerFn().handler(() =>
renderServerComponent(
<div className="dashboard">
<h1>Analytics</h1>
<div className="metrics-grid">
<Suspense fallback={<MetricSkeleton label="Active Users" />}>
<SlowMetric label="Active Users" delay={500} />
</Suspense>
<Suspense fallback={<MetricSkeleton label="Revenue" />}>
<SlowMetric label="Revenue" delay={1500} />
</Suspense>
<Suspense fallback={<MetricSkeleton label="Conversion" />}>
<SlowMetric label="Conversion" delay={2500} />
</Suspense>
</div>
</div>,
),
)每个指标独立流式呈现。仪表盘外壳立刻出现,然后各个指标随着数据加载逐个弹出。
用异步生成器流式传输
用异步生成器逐个流式传输服务器组件。客户端在生成器产生每个组件时接收并渲染它。
什么时候用: 无界或很大的结果集,希望条目增量渲染。当每个条目的处理时间不同,或总数未知时很有用。
import {
CompositeComponent,
createCompositeComponent,
} from '@tanstack/react-start/rsc'
const streamNotifications = createServerFn().handler(async function* () {
// Yield initial batch immediately
const recent = await db.notifications.getRecent(3)
for (const notification of recent) {
yield await createCompositeComponent<{
renderActions?: (data: { id: string }) => React.ReactNode
}>((props) => (
<div className="notification">
<h3>{notification.title}</h3>
<p>{notification.message}</p>
{props.renderActions?.({ id: notification.id })}
</div>
))
}
// Stream older notifications with delays
const older = await db.notifications.getOlder(5)
for (const notification of older) {
await new Promise((resolve) => setTimeout(resolve, 300))
yield await createCompositeComponent<{
renderActions?: (data: { id: string }) => React.ReactNode
}>((props) => (
<div className="notification">
<h3>{notification.title}</h3>
<p>{notification.message}</p>
{props.renderActions?.({ id: notification.id })}
</div>
))
}
})
export const Route = createFileRoute('/notifications')({
component: NotificationsPage,
})
function NotificationsPage() {
const [notifications, setNotifications] = React.useState<Array<unknown>>([])
const [isStreaming, setIsStreaming] = React.useState(false)
const startStreaming = React.useCallback(async () => {
setNotifications([])
setIsStreaming(true)
const stream = await streamNotifications()
for await (const notification of stream) {
setNotifications((prev) => [...prev, notification])
}
setIsStreaming(false)
}, [])
return (
<div>
<button onClick={startStreaming} disabled={isStreaming}>
{isStreaming ? 'Streaming...' : 'Load Notifications'}
</button>
{notifications.map((notificationSrc, i) => (
<CompositeComponent
key={i}
src={notificationSrc}
renderActions={({ id }) => (
<button onClick={() => markAsRead(id)}>Mark read</button>
)}
/>
))}
</div>
)
}通知逐个出现。前三条立刻显示,然后更多内容继续流入。每个都通过 render props 支持客户端交互。
错误处理
服务器组件中的错误——无论是数据获取还是渲染期间——都会传播到客户端。
路由级错误
如果服务器组件在 loader 中加载失败,路由的 errorComponent 会渲染:
export const Route = createFileRoute('/')({
loader: async () => ({
// If this fails, the errorComponent renders
Greeting: await getGreeting(),
}),
errorComponent: ({ error }) => <div>Failed to load: {error.message}</div>,
component: HomePage,
})组件级错误
要隔离错误(比如防止单个失败的 widget 拖垮整个页面),你必须使用延迟加载。
让 loader 返回 Promise 而不是 await 它,路由组件会立即渲染。如果 Promise 之后被拒绝,组件内的 ErrorBoundary 会捕获它。
// 1. Loader returns a Promise (don't await!)
export const Route = createFileRoute('/dashboard')({
loader: () => ({
// If this fails, only the specific ErrorBoundary below catches it
WidgetPromise: getWidget(),
}),
component: DashboardPage,
})
// 2. Component handles the potential failure
function DashboardPage() {
const { WidgetPromise } = Route.useLoaderData()
return (
<ErrorBoundary fallback={<div>Widget unavailable</div>}>
<React.Suspense fallback={<Skeleton />}>
<Deferred promise={WidgetPromise} />
</React.Suspense>
</ErrorBoundary>
)
}技巧
使用 React.cache
React.cache 可以在服务器组件内部用于请求级记忆化(memoization)。当多个组件需要同一个开销大的计算时很有用:
import { cache } from 'react'
const getUser = cache(async (userId: string) => {
return db.users.findById(userId)
})
// Both components share the same cached result within a single request
async function UserHeader() {
const user = await getUser('123') // Fetches from DB
return <h1>{user.name}</h1>
}
async function UserSidebar() {
const user = await getUser('123') // Returns cached result
return <aside>{user.bio}</aside>
}服务器组件中的路由链接
TanStack Router 的 Link 组件可以在服务器组件内部工作。链接会被序列化,并在客户端水合以支持客户端导航:
import { Link } from '@tanstack/react-router'
const getNavigation = createServerFn().handler(async () => {
const pages = await db.pages.list()
return renderServerComponent(
<nav>
{pages.map((page) => (
<Link key={page.id} to="/pages/$pageId" params={{ pageId: page.id }}>
{page.title}
</Link>
))}
</nav>,
)
})服务器组件中的 CSS
CSS Modules 和全局 CSS 导入可以在服务器组件中工作。样式会被提取并发送给客户端:
import styles from './Card.module.css'
const getCard = createServerFn().handler(async () => {
return renderServerComponent(
<div className={styles.card}>
<h2 className={styles.title}>Server Rendered</h2>
</div>,
)
})规则与限制
服务器上的插槽是不透明的
服务器无法检查插槽内容。React.Children.map() 和 cloneElement() 在 props.children 上不会生效:
// Won't work - children is a placeholder on the server
createCompositeComponent((props: { children?: React.ReactNode }) => (
<div>
{React.Children.map(props.children, (child) =>
React.cloneElement(child, { extra: 'prop' }),
)}
</div>
))
// Do this instead - use render props
createCompositeComponent<{
renderItem?: (data: { extra: string }) => React.ReactNode
}>((props) => <div>{props.renderItem?.({ extra: 'prop' })}</div>)Render prop 参数必须可序列化
通过 render props 或组件传给插槽的参数会经过 React 的 Flight 协议。只能使用可序列化的值:字符串、数字、布尔值、null、普通对象和数组。
工作原理
当服务器组件访问插槽 props 时,它访问的是一个代理(proxy):
- 读取
props.children会创建一个「调用方提供什么 children」的占位符 - 调用
props.renderFn(args)会创建一个记录args的占位符
TanStack Start 发送一个带这些占位符的 React Flight 流。在客户端,占位符会被你渲染时传入的实际 props 替换。
当前状态
服务器组件在 TanStack Start 中仍是实验性的,并且会一直保持到 v1 早期。
序列化: 使用 React 原生的 Flight 协议。TanStack Start 的自定义序列化还不能用于服务器组件。基本类型、Date 和 React 元素可用。自定义序列化会在未来的版本中提供。
API: RSC 辅助函数的 API 将来可能会有所调整。
底层 Flight 流 API
对于高级场景(自定义流式协议、API 路由集成、外部 RSC 感知系统),TanStack Start 暴露了底层的 Flight 流 API。大多数情况下,请优先使用高层辅助函数,它们会自动处理缓存、流式传输,以及(对于复合组件)插槽。
从 @tanstack/react-start/rsc 导入:
| 函数 | 可用环境 | 说明 |
|---|---|---|
renderToReadableStream | 仅服务器函数 | 把 React 元素渲染为 Flight 流 |
createFromFetch | 客户端 | 从 Promise<Response> 解码 Flight 流 |
createFromReadableStream | 客户端/SSR | 从 ReadableStream 解码 Flight 流 |
createFromFetch 是 createFromReadableStream 的便捷包装,它直接接收一个 fetch promise,并在内部提取 body 流。
示例
// src/routes/api/rsc.ts - API route with Flight stream
import { createAPIFileRoute } from '@tanstack/react-start/api'
import { createServerFn } from '@tanstack/react-start'
import { renderToReadableStream } from '@tanstack/react-start/rsc'
const getFlightStream = createServerFn({ method: 'GET' }).handler(async () => {
return renderToReadableStream(<div>Server Rendered Content</div>)
})
export const APIRoute = createAPIFileRoute('/api/rsc')({
GET: async () => {
const stream = await getFlightStream()
return new Response(stream, {
headers: { 'Content-Type': 'text/x-component' },
})
},
})// Client: fetch and decode the Flight stream
import { createFromFetch } from '@tanstack/react-start/rsc'
async function fetchRSC() {
return createFromFetch(fetch('/api/rsc'))
}