先守住数据,再下沉交互:RSC 组件分层的实战决策法 📅 2026/8/24 4:23:00 原文链接先守住数据再下沉交互RSC 组件分层的实战决策法在真实项目里React Server ComponentsRSC最容易被问成一句话“这个组件该不该加use client”但这往往已经太晚了。更好的起点不是组件名称、页面位置甚至不是首屏性能而是先问这段数据、判断和状态分别属于谁又必须在哪个环境里存活在 Next.js App Router 中page和layout默认是 Server Component。服务端组件可以在服务端读取数据并组织 UI需要状态、事件处理、生命周期逻辑或浏览器 API 时再用 Client Component 建立局部交互边界。use client划分的是客户端模块依赖图的入口而不是给某个 JSX 节点贴上“浏览器运行”的标签。(Next.jsServer and Client Components)因此RSC 分层的第一原则可以写成先让数据与可信判断留在服务端再把不可避免的连续交互下沉为尽可能小的客户端岛。这套原则不承诺“所有页面都更快”但能让权限、数据流、打包边界和后续改造更可控。不要先按“组件类型”分先回答四个问题设计一个新组件或重构一个既有 CSR 页面时依次判断下面四件事。1. 数据由谁拥有如果组件要直接读取以下资源默认应从服务端开始设计数据库、内部 RPC 或仅面向内网的服务API Key、服务端令牌、私有环境变量由 Cookie 或服务端会话解析出的可信身份需要按当前用户权限裁剪的字段不应该完整暴露给浏览器的领域对象。服务端不只是“更方便请求数据”的位置它还是数据最小暴露的边界。传给客户端的应该是为展示和交互裁剪过的 DTO例如id、标题、状态、允许执行的动作而不是 ORM 实体、完整用户档案或内部权限规则。Next.js 的认证指南建议将授权逻辑集中在数据访问层并通过 DTO 只返回调用方真正需要的数据。(Next.jsAuthentication)2. 判断是否必须在可信环境完成“是否显示删除按钮”可以由服务端根据角色决定但“用户是否真的能删除这条记录”必须在写入入口再次判断。浏览器中的条件渲染只能改善体验不能构成授权。用户可以伪造请求、绕过页面入口或直接调用暴露的 HTTP 接口。因此Server Component可以负责读取会话、裁剪可见数据、决定展示哪些操作入口Server Action或Route Handler必须在每次读取或写入时验证身份与权限客户端拿到的canDelete应理解为 UI 提示而不是安全凭证。Next.js 明确要求把 Server Actions 和 Route Handlers 按公开 API 的安全标准处理并在每个入口完成授权校验。(Next.jsAuthentication)3. 状态需要活多久这是判断客户端边界最有效、也最容易被忽略的问题。只服务于一次请求的状态例如当前 URL 参数对应的查询条件、当前用户可见字段、服务端格式化后的金额适合留在服务端。跨多次操作持续存在的局部状态例如已选中的多行、未提交的编辑草稿、弹窗开关、拖拽中的位置、图表缩放范围适合放进 Client Component。需要和 URL 同步的状态例如搜索词、分页、排序、筛选条件通常应以 URL 为事实来源客户端负责更新 URL服务端根据新参数重新读取结果。关键不是“状态是否存在”而是它是否需要在浏览器中以毫秒级反馈持续演化。4. 用户是否需要连续、即时的交互以下能力是明确的客户端信号事件处理、useState、useEffect、浏览器 API、依赖它们的自定义 Hook以及浏览器专属第三方 SDK。(Next.jsServer and Client Components)不过“页面上有按钮”不等于“整页必须客户端化”。一个只有“展开详情”按钮的卡片可以让卡片内容仍由服务端生成只把展开、收起的壳做成客户端组件。真正要下沉的是交互生命周期而不是把与它相邻的大块内容一并拖到客户端。三种服务端能力别混成一个概念RSC 项目里常见的混乱是把“运行在服务端”当成同一件事。实际上它们的职责不同。能力主要职责适合放什么Server Component读取数据、组织服务端 UI、输出展示结果页面壳、数据片段、权限裁剪后的操作区Server Function / Server Action接收交互触发的服务端调用执行受校验的变更新建、审批、删除、保存草稿Route Handler提供具有 HTTP 语义的服务端入口Webhook、开放 API、下载接口、非 React 调用方需要特别纠正两个误解Server Component 不需要use server。use server标记的是 Server Function不是“把组件变成服务端组件”的开关。(Reactuse server)Server Action 不是通用数据获取工具。它更适合处理交互发起的状态变更读取和渲染仍优先由 Server Component 或专门的数据访问层完成。换句话说组件决定“如何呈现”Action 或 Handler 决定“如何安全地变更或对外提供能力”。组合规则use client划的是依赖图不是视觉树另一个常见误解是“Client Component 不能包含 Server Component。”更准确的说法是客户端模块不能直接导入并执行服务端组件模块但服务端可以先渲染 Server Component再把生成的 JSX 作为children或其他 props 传给 Client Component一旦文件写了use client它导入的模块及其子依赖会进入客户端模块图因此边界应尽量靠近实际交互点。(ReactServer Components)例如一个客户端弹窗壳可以接收服务端生成的详情// Server Component export default async function Page() { const report await getReport() return ( ReportDialog ReportSummary report{report} / /ReportDialog ) }// Client Component use client export function ReportDialog({ children }: { children: React.ReactNode }) { const [open, setOpen] useState(false) return ( button onClick{() setOpen(true)}查看报告/button {open ? div roledialog{children}/div : null} / ) }这里ReportDialog管理浏览器中的开关状态ReportSummary则由服务端页面准备数据并渲染。不要因为它们在视觉上是父子关系就错误地把报告详情整体客户端化。跨边界传递的数据也不是任意 JavaScript 值。Client Component 的 props 必须可被 React 序列化普通回调、类实例和多数带原型的对象都不应直接透传。把 DTO 作为边界契约能避免服务端领域模型意外泄露也能让组件职责更清晰。(Next.jsuse client)案例把一个 SaaS 数据列表拆成正确的层假设你要做一个“订单审核”页面包含权限控制、关键词搜索、状态筛选、分页排序、行选择、批量审核、编辑弹窗和导出。第一层页面壳留在服务端app/orders/page.tsx适合承担从 Cookie 或服务端会话得到当前用户校验是否有访问订单列表的权限解析searchParams查询当前页数据根据角色裁剪订单字段计算每条记录允许出现哪些动作。这一层不需要use client。它拥有请求上下文也最接近数据源和授权规则。第二层结果表格优先仍是服务端片段订单号、客户名称、金额、状态、审核人、服务端计算出的可执行操作本质上都是当前请求的展示结果。它们不因用户在浏览器中点击一次复选框而必须变成客户端组件。表格主体可以保持服务端渲染如果某一列只展示数据就不要因为“表格通常很复杂”而默认整表客户端化。第三层把控制器做成小型客户端岛以下部分通常需要 Client Component搜索输入框的防抖和即时输入体验筛选下拉框的临时选择状态列显隐偏好多行勾选批量操作工具栏编辑、确认、导出进度等弹窗状态。但这些控制器不必拥有全部订单数据。一个更稳健的结构是OrdersPage服务端鉴权、查询、DTO ├─ OrdersFilterController客户端编辑筛选条件、更新 URL ├─ OrdersTable服务端按 URL 参数渲染结果 │ └─ RowActionMenu客户端菜单开关、确认弹窗 └─ BulkActionController客户端维护选中 ID、发起提交这里的重点是筛选器管理“用户正在输入什么”服务端页面管理“当前 URL 对应什么结果”。当用户提交筛选条件时客户端更新 URL路由刷新后服务端依据新的查询参数和当前会话重新查询。这避免了两类常见问题首屏数据在客户端重复请求以及浏览器自行决定用户本不该看到的结果。第四层批量审核走受校验的写入口批量审核按钮由客户端触发没有问题但提交到 Server Action 或 Route Handler 后服务端仍应校验输入格式例如订单 ID 数量、状态值和备注长度重新读取会话与角色验证用户对每一条订单都有操作权限在事务或一致性策略中执行变更按产品要求刷新相关数据。如果操作者提交后必须立刻看到自己刚修改的状态可在 Server Action 中使用updateTag处理 read-your-own-writes如果允许短暂陈旧并在后台更新则可使用revalidateTag。这是写后数据一致性的选择不是决定组件该放服务端还是客户端的依据。(Next.jsRevalidating)灰区不靠背答案靠识别触发条件场景默认归属何时需要客户端化不能下放的约束搜索、筛选、分页、排序URL 与查询结果在服务端输入防抖、临时筛选面板、无刷新 URL 操作服务端必须按最新参数和权限重新查询表单表单结构与初始值可在服务端即时校验、草稿、复杂联动、文件预览服务端必须校验输入与授权图表聚合数据和报表说明在服务端缩放、悬浮、刷选、浏览器图表库只传绘图所需的最小数据集富文本编辑器内容展示可在服务端编辑器运行时、选区、快捷键、浏览器插件保存时在服务端清洗、校验并授权权限按钮服务端决定是否展示菜单开关、确认交互隐藏按钮不等于禁止操作国际化服务端读取语言和格式化展示用户即时切换、浏览器本地偏好交互不要把敏感文案规则或权限说明当作客户端事实来源埋点服务端可记录请求级事件点击、滚动、曝光等浏览器事件用薄客户端封装避免 SDK 吞没整页依赖图loading/errorloading可围绕服务端异步片段设置error是客户端错误边界错误后的重试按钮、局部恢复交互错误信息不得泄露内部实现和敏感数据需要注意Next.js App Router 中的error.tsx必须是 Client Component因为它需要作为 React 错误边界处理运行时错误而loading.tsx通常用于为路由段提供加载 UI。不要把二者都简单视为“服务端组件能力”。第三方富文本编辑器、地图、图表和埋点 SDK 经常依赖window、事件或生命周期。这时推荐给它们加一层薄的 Client wrapper而不是把页面或全局 layout 标记为use client。(Next.jsServer and Client Components)五个信号说明你的边界已经开始失控1. 为了一个点击事件把page.tsx标成了use client这意味着客户端边界向上吞没了页面的数据读取、展示组件和依赖。应把点击行为抽到更小的按钮、菜单或弹窗组件。2. 首屏已有的数据又在useEffect中请求一次这通常说明服务端查询结果没有成为页面事实来源客户端又建立了一套并行状态。除非确实需要独立轮询或实时订阅否则优先消除重复请求。3. 服务端数据一路 props drilling最后才到一个交互按钮先不要急着把中间树都客户端化。检查按钮真正需要的是完整对象还是一个 ID、展示文案和有限的权限标记多数时候缩小 DTO 比扩大客户端边界更正确。4. 为跨边界传值开始传 ORM 实体、类实例或普通函数这不是“序列化技巧不足”而是在提醒你服务端模型与客户端视图模型没有分开。建立明确 DTO回调则留在客户端写操作改为 Server Function。5. 只靠前端隐藏按钮实现“权限控制”这说明展示层承担了不该承担的安全职责。服务端应在数据读取和变更入口分别校验而不是把权限寄托在用户看不见某个按钮上。可执行检查单每次设计组件前按这个顺序走先列数据它来自哪里哪些字段不能离开服务端再列判断哪些规则必须基于可信身份或内部策略执行再列状态它是一次请求的结果还是浏览器中持续演化的交互状态最后列交互真正需要事件、Effect、浏览器 API 或第三方 SDK 的最小单元是什么划 DTO 边界客户端拿到的是渲染和交互所需的最小可序列化数据而非服务端对象。设计写入口Action 或 Handler 独立做输入校验、身份校验和权限校验。定义写后可见性用户是否必须立即看到自己的修改再选择刷新或失效策略。RSC 的成熟用法不是追求“更多组件跑在服务端”也不是把客户端交互视为失败。真正的目标是让每一层只承担它天然擅长的职责服务端守住数据、密钥、授权和请求级结果客户端承接即时、连续、面向用户操作的局部状态写入口守住校验、一致性与权限两者之间只流动必要、可序列化、可解释的数据。当团队先划清数据边界再决定组件运行位置use client就不再是一场全页范围的性能争论而会成为一次小而明确的架构决策。参考资料ReactServer ComponentsReactuse serverNext.jsServer and Client ComponentsNext.jsuse clientNext.jsAuthenticationNext.jsRevalidatingNext.jsError Handling