Back

React Server Components 的常见错误

React Server Components 的常见错误

你已经采用了 Next.js App Router。Server Components 是默认选项。一切本应更快、更简单、更高效。

然而,你却在凌晨 2 点调试 hydration 不匹配问题,疑惑为什么打包体积反而增大了,并质疑那个 'use client' 指令是否放对了位置。

React Server Components 代表了 React 应用工作方式的根本性转变。服务器/客户端边界不再仅仅是部署层面的考虑——它是你在每个组件中做出的架构决策。搞错这一点会导致难以察觉的 bug、性能倒退,以及与框架对抗而非利用框架的代码。

本文介绍了我在生产代码库中见过的最常见的 React Server Components 错误以及如何避免它们。

核心要点

  • Server Components 是 Next.js App Router 中的默认选项——将 'use client' 尽可能向下推到组件树中,以最小化打包体积。
  • 使用 server-only 包来防止敏感的服务器代码意外暴露到客户端打包中。
  • 在从 Server Components 传递到 Client Components 时,始终转换不可序列化的值(如函数和类实例)。
  • Server Actions('use server')是 RPC 风格的端点,而非 Server Components——验证所有输入,永远不要信任客户端数据。
  • 使用 revalidatecache: 'no-store' 明确缓存行为,因为 Next.js 的默认值在不同版本间有所变化。

理解 Server Components 与 Client Components

在深入探讨陷阱之前,让我们先建立基准认知。在 App Router 中,组件默认是 Server Components。它们在服务器上运行,无法访问浏览器 API,并且不会向客户端发送任何 JavaScript。

Client Components 需要 'use client' 指令。它们可以使用 useStateuseEffect 等 hooks,访问浏览器 API,并处理用户交互。

它们之间的边界是大多数错误发生的地方。

过度使用 ‘use client’ 指令

Next.js App Router RSC 最常见的陷阱是过早使用 'use client'。组件需要 useState?标记为客户端组件。需要 onClick 处理器?客户端组件。

问题在于:'use client' 会创建一个边界。该组件导入的所有内容都会成为客户端打包的一部分,即使这些导入本可以留在服务器上。

// ❌ 整个页面变成了客户端组件
'use client'

import { useState } from 'react'

export default function ProductPage({ product }) {
  const [quantity, setQuantity] = useState(1)
  
  return (
    <div>
      <ProductDetails product={product} />
      <ProductReviews productId={product.id} />
      <QuantitySelector value={quantity} onChange={setQuantity} />
    </div>
  )
}
// ✅ 只有交互部分是客户端组件
import ProductDetails from './ProductDetails'
import ProductReviews from './ProductReviews'
import QuantitySelector from './QuantitySelector'

export default function ProductPage({ product }) {
  return (
    <div>
      <ProductDetails product={product} />
      <ProductReviews productId={product.id} />
      <QuantitySelector /> {/* 这是唯一的客户端组件 */}
    </div>
  )
}

'use client' 尽可能向下推到组件树中。将交互性隔离到需要它的最小组件中。

在 Client Components 中导入仅限服务器的代码

当客户端组件导入一个模块时,整个模块(及其依赖项)都会发送到浏览器。导入数据库客户端或读取环境密钥的文件?你刚刚将仅限服务器的代码暴露给了客户端依赖图。

// lib/db.js
import 'server-only' // 添加此项以防止意外的客户端导入

export async function getUsers() {
  return db.query('SELECT * FROM users')
}

server-only 包(由 Next.js 提供)会在模块被导入到客户端组件时导致构建错误。对任何绝不能到达浏览器的代码使用它。

跨边界传递不可序列化的值

Server Components 通过序列化将 props 传递给 Client Components。函数、类实例、MapSet 无法跨越这个边界。

// ❌ 类实例不可序列化
export default async function UserProfile({ userId }) {
  const user = await getUser(userId)
  return <ClientProfile user={user} /> // user 是一个类实例
}

// ✅ 转换为普通对象
export default async function UserProfile({ userId }) {
  const user = await getUser(userId)
  return (
    <ClientProfile 
      user={{
        id: user.id,
        name: user.name,
        createdAt: user.createdAt.toISOString()
      }} 
    />
  )
}

误解 React Server Actions

'use server' 指令将函数标记为 Server Actions——可从客户端调用但在服务器上执行。它不会使组件成为 Server Component。Server Components 不需要任何指令,因为它们是默认的。

// 这是一个 Server Action,不是 Server Component
async function submitForm(formData) {
  'use server'
  await db.insert({ email: formData.get('email') })
}

Server Actions 实际上是 RPC 风格的端点。像对待 API 路由一样对待它们:验证输入、处理错误,永远不要信任客户端数据。

忽略 RSC 缓存模型

Next.js 的缓存行为已经发生了显著变化。不要假设 fetch 调用默认被缓存——这因 Next.js 版本、路由段配置和运行时设置而异。明确数据新鲜度。

// 明确缓存意图
const data = await fetch(url, { 
  next: { revalidate: 3600 } // 缓存 1 小时
})

// 或完全退出缓存
const data = await fetch(url, { cache: 'no-store' })

在 Server Actions 中使用 revalidatePath()revalidateTag() 来在数据变更后使缓存失效。RSC 缓存模型需要对数据新鲜度做出有意识的决策。

结论

React Server Components 回报那些仔细思考代码运行位置的开发者。默认使用 Server Components。向下推客户端边界。在边缘序列化数据。验证 Server Action 输入。明确缓存策略。

这种思维模型需要时间来内化,但回报——更小的打包体积、更快的加载速度、更简单的数据获取——值得投入。

常见问题

部分可以。Server Components 不能使用状态或副作用 hooks,如 useState 或 useEffect,因为它们只在服务器上运行。但是,像 useContext 这样的 hooks 是支持的。如果你的组件需要状态、副作用或浏览器 API,你必须添加 use client 指令使其成为 Client Component。尽可能保持这些交互部分小而独立。

问问自己组件是否需要交互性、浏览器 API 或像 useState 或 useEffect 这样的 React hooks。如果需要,它必须是 Client Component。如果它只渲染数据或从数据库获取数据,保持它作为 Server Component。如有疑问,从 Server Component 开始,只在构建或运行时明确需要时才添加 use client。

最常见的原因是将 use client 放置在组件树中过高的位置。当父组件成为 Client Component 时,其所有导入都会加入客户端打包。审查你的 use client 指令,并将它们向下推到最小的交互组件。还要检查客户端代码中是否意外导入了仅限服务器的库。

use client 指令标记组件在浏览器中运行,可以访问 hooks 和浏览器 API。use server 指令将函数标记为 Server Action,可从客户端调用但在服务器上执行。Server Components 完全不需要指令,因为它们是 Next.js App Router 中的默认选项。

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay