React 状态管理方案对比与选型
Redux、Zustand、Jotai、Recoil、Context 的状态边界设计与大型项目选型决策树。
RSC 的渲染模型、Server/Client 组件边界、数据获取模式与 Next.js App Router 落地经验。
React Server Components(RSC)是 React 18+ 最重要的架构变革之一。它不是在 SSR 基础上加功能,而是重新定义了组件在哪里运行、数据在哪里获取、Bundle 里包含什么。
| 类型 | 运行环境 | 能否用 useState | 能否访问 DB | 是否进入客户端 Bundle |
|---|---|---|---|---|
| Server Component | 服务端 | 否 | 是 | 否 |
| Client Component | 浏览器 | 是 | 否 | 是 |
// app/posts/page.tsx — Server Component(默认)
import { db } from "@/lib/db";
export default async function PostsPage() {
const posts = await db.post.findMany(); // 直接访问数据库
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// components/LikeButton.tsx — Client Component
"use client";
import { useState } from "react";
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>{liked ? "已赞" : "点赞"}</button>
);
}
Pages Router: getServerSideProps → 组件渲染
App Router: Server Component 直接 async/await → 流式渲染
Client: SWR/React Query → 客户端缓存
RSC 的优势:
'use client''use client' 推到叶子节点,最大化 Server 渲染范围// 正确:Client 组件包裹 Server 组件的 children
"use client";
export function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return open ? <dialog>{children}</dialog> : null;
}
// page.tsx (Server)
<Modal>
<ServerOnlyContent /> {/* 作为 children 传入,不违反边界 */}
</Modal>;
RSC 配合 Suspense 实现流式 HTML:
export default function DashboardPage() {
return (
<div>
<Header /> {/* 立即发送 */}
<Suspense fallback={<ChartSkeleton />}>
<SlowChart /> {/* 异步加载,不阻塞 Header */}
</Suspense>
</div>
);
}
浏览器会逐步收到 HTML 片段,用户感知的首屏时间大幅缩短。
useEffect / useState → 编译报错'use client',导致整个子树变成客户端组件 → 失去 RSC 优势| 维度 | React RSC | Vue SSR (Nuxt) |
|---|---|---|
| 组件粒度 | Server/Client 显式标记 | 统一 SSR + hydration |
| 数据获取 | 组件内 async | useAsyncData / useFetch |
| Bundle 优化 | 服务端组件零 JS | 全量 hydration |
| 生态成熟度 | Next.js 15+ 较成熟 | Nuxt 3 稳定 |
RSC + Suspense 流式输出时,错误不能等整页失败:
// app/dashboard/error.tsx
'use client';
export default function DashboardError({ error, reset }: { error: Error; reset: () => void }) {
return (
<div role="alert">
<p>模块加载失败:{error.message}</p>
<button onClick={reset}>重试</button>
</div>
);
}
Server Component throw 会冒泡到最近 error.tsx;部分模块失败时用 Parallel Routes + 独立 error slot,别拖垮整页。
静态 shell 先出,动态块 Suspense 边界内流式填充——营销页 LCP 我们 2.4s → 1.6s,动态推荐区晚 200ms 到达可接受。
'use client' 包一层——等于放弃 RSCRSC 落地关键:划清 Server/Client 边界 + Suspense 粒度 + error 隔离,不是给每个文件加 async 那么简单。