文章目录

React Server Components与Client Components的边界划分

发布于 2026-04-19 14:26:08 · 浏览 6 次 · 评论 0 条

React Server Components与Client Components的边界划分

React Server Components (RSC) 和 Client Components (CC) 的混合使用是现代 React 开发的核心。错误地划分边界会导致应用变慢、交互失效甚至逻辑错误。本文将提供一套严格的判断标准与操作步骤,帮助你精确划分两者的边界。


核心原则:默认服务端,按需客户端

在 React App Router 或 Next.js 等现代框架中,所有组件默认都是服务端组件。这意味着你不需要刻意去标记服务端组件,只需要在必须用到浏览器特性时,才将其标记为客户端组件。


第一步:判断是否需要交互性

这是划分边界的首要标准。这里的“交互”特指需要 React Hooks 来管理的状态或副作用。

  1. 检查组件内部是否使用了 useStateuseReduceruseRef 等 Hooks。
  2. 检查组件内部是否使用了 useEffect 来处理副作用(如数据获取、DOM 操作)。
  3. 执行判断:
    • 如果上述任意一项为“是”,则该组件必须是客户端组件。
    • 打开该文件,在代码最顶端添加 'use client'; 指令。
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      点击次数: {count}
    </button>
  );
}

第二步:检查浏览器 API 依赖

有些组件虽然不显式使用 Hooks,但依赖浏览器提供的全局对象。

  1. 审查代码中是否包含 windowdocumentnavigator 等对象。
  2. 审查是否使用了基于这些对象的第三方库(如 localStorageAudioVideo 或特定的动画库)。
  3. 执行判断:
    • 如果代码直接调用了浏览器 API,则该组件必须是客户端组件。
    • 添加 'use client'; 指令。

第三步:排查事件监听器

React 事件处理函数(如 onClickonChangeonSubmit)只能在客户端执行。

  1. 查找 JSX 中的 on* 属性。
  2. 确认这些事件处理函数是否直接定义在当前组件内部,或者作为 props 从父组件传入。
  3. 执行判断:
    • 如果组件内部直接定义了事件处理函数(例如 onClick={() => alert('Hi')}),它必须是客户端组件。
    • 注意:如果组件仅渲染 HTML,且事件处理函数完全由父组件(客户端组件)传入,那么当前组件可以是服务端组件(但这属于高级用法,初学者建议将含交互的叶子节点都设为客户端)。

辅助决策流程图

当你在犹豫该组件属于哪一类时,请严格按照以下流程进行决策。

graph TD A[开始: 创建新组件] --> B{是否使用\nuseState/useEffect?} B -- 是 --> C["必须为:\nClient Component"] B -- 否 --> D{是否使用\n浏览器 API?\n(window/document)} D -- 是 --> C D -- 否 --> E{是否使用\n事件监听器?\n(onClick/onChange)} E -- 是 --> C E -- 否 --> F["默认为:\nServer Component"] C --> G["在文件顶部添加:\n'use client'"] F --> H["无需添加任何指令"]

第四步:对比清单与数据获取模式

理解数据获取方式的差异是优化性能的关键。

特性 Server Components Client Components
默认状态 默认开启 需添加 'use client'
数据获取 直接在组件内使用 async/await 依赖 useEffect 或 SWR/React Query
网络请求 发生在服务端,访问数据库/后端安全 发生在浏览器端,需通过 API 调用
打包体积 代码不发送到浏览器,0 KB 体积 代码会发送到浏览器,增加包体积
密钥安全 可安全存储 API Key/密钥 严禁存储密钥,暴露给用户
交互能力 无(无 Hooks,无事件) 完整支持

数据获取示例对比

服务端组件写法(推荐):

// 这是一个 Server Component,无需 'use client'
async function UserProfile({ id }: { id: string }) {
  // 直接后端获取数据
  const res = await fetch(`https://api.example.com/user/${id}`);
  const user = await res.json();

  return <div>用户: {user.name}</div>;
}
```

**客户端组件写法(仅必要时使用):**

```tsx
'use client';

import { useEffect, useState } from 'react';

function UserProfile({ id }: { id: string }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 浏览器端获取数据
    fetch(`https://api.example.com/user/${id}`)
      .then(res => res.json())
      .then(data => setUser(data));
  }, [id]);

  if (!user) return <div>加载中...</div>;
  return <div>用户: {user.name}</div>;
}

第五步:处理组件嵌套与 Props 传递

最复杂的场景是服务端组件与客户端组件互相嵌套。记住一条黄金法则:数据自上而下流动,交互自下而上隔离。

场景 1:Server 包裹 Client(常见)

布局是 Server Component,其中的按钮是 Client Component。

  1. 保留父级文件为默认(服务端)。
  2. 创建子组件文件,添加 'use client'
  3. 服务端组件中导入使用客户端组件。
// 父组件: layout.tsx (Server Component)
import InteractiveButton from './InteractiveButton';

export default function Layout() {
  return (
    <div>
      <h1>服务端渲染的标题</h1>
      <InteractiveButton />
    </div>
  );
}

场景 2:Client 包裹 Server(高级:Props 模式)

客户端组件不能直接导入服务端组件作为子组件渲染。如果客户端组件需要接收服务端生成的复杂 UI 作为子元素,必须通过 children 属性传递。

  1. 定义客户端组件,使其接收 children 属性。
  2. 定义服务端组件,负责获取数据。
  3. 服务端组件中包裹客户端组件,并将服务端生成的 JSX 作为 children 传入
// 1. 客户端组件: ClientWrapper.tsx
'use client';

export default function ClientWrapper({ children }: { children: React.ReactNode }) {
  const [show, setShow] = useState(false);

  return (
    <div>
      <button onClick={() => setShow(!show)}>切换显示</button>
      {show && <div className="client-style">{children}</div>}
    </div>
  );
}

// 2. 服务端组件: page.tsx
import ClientWrapper from './ClientWrapper';

async function ServerData() {
  // 模拟数据库查询
  return <div>这是从数据库获取的敏感数据:$1234</div>;
}

export default function Page() {
  return (
    <ClientWrapper>
      {/* ServerData 在服务端渲染,结果作为 props 传给 ClientWrapper */}
      <ServerData />
    </ClientWrapper>
  );
}

关键点:

  • 严禁在 Client Component 内部 import 一个 Server Component 直接使用 <ServerComponent />
  • 必须使用 <ClientWrapper>{<ServerComponent />}</ClientWrapper> 的形式。

第六步:优化第三方库的使用

许多第三方库(如 UI 组件库 antdMaterial UI,或动画库 Framer Motion)内部依赖浏览器 API 或 Hooks。

  1. 查阅第三方库文档,确认其是否支持 Server Components。
  2. 如果不支持,你必须创建一个客户端组件来包装该库,然后在上层的服务端组件中引用这个包装器。
  3. 示例:使用一个不支持 RSC 的滑块组件。
// 1. 创建包装器: SliderWrapper.tsx
'use client';
import { Slider } from 'some-third-party-lib';

export default function SliderWrapper(props: any) {
  return <Slider {...props} />;
}

// 2. 在服务端组件中使用
import SliderWrapper from './SliderWrapper';

export default function Page() {
  return (
    <main>
      <SliderWrapper min={0} max={100} />
    </main>
  );
}

通过以上步骤,你可以建立起清晰的组件边界思维:尽可能让代码在服务端运行以获得最佳性能,仅在涉及用户交互和浏览器特性时才将必要的部分下放到客户端。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文