译:关于 React 服务器组件的 5 个误解

原文:https://www.builder.io/blog/nextjs-react-server-components
作者:VISHWAS GOPINATH
译者:ChatGPT 4 Turbo

随着 React 19 和 Next.js 15 的稳定版本发布临近,React 团队介绍了 React 架构中最重要的变化之一:React 服务器组件(RSC)范式,它区分了服务器组件和客户端组件。

服务器组件 只在服务器上运行,对包大小没有任何影响。它们的代码永远不会被下载到客户端,有助于减少包大小并提升启动时间。

相比之下,客户端组件 是您已经习惯的典型组件。它们可以访问 React 的全部功能:状态、效果、访问 DOM 等。

这种新方法旨在利用服务器和客户端环境的优势,优化效率、加载时间和交互性。

尽管它为 React 开发带来了重大改进和灵活性,但它也引入了一些容易被误解的概念。在这篇文章中,我们将解释关于 React 服务器组件(RSC)的五个常见误解,以帮助阐明这种新架构的一些细节问题。

误解 1:应该优先选择服务器组件,而客户端组件应该谨慎使用

您可能会认为,为了从新的 RSC 架构中获得最多的好处,您应该将所有组件转换为服务器组件,并谨慎使用客户端组件。然而,最优的方法实际上是更加平衡的:

  • 虽然服务器组件提供了很多好处,但它们并不适合所有场景,特别是那些需要交互性的场景。React 服务器组件是一种新型的 React 组件,只在服务器上操作,获取数据并完全在服务器上渲染以提高性能。
// UserProfile.js (服务器组件)

// 该组件可以直接访问数据库或 API
// 而不将敏感信息暴露给客户端
async function UserProfile({ userId }) {
  const response = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
	const user = response.user;
	
  return (
    <div>
      <h1>{user.name}</h1>
      <p>电子邮件:{user.email}</p>
      <p>位置:{user.location}</p>
      <LikeButton initialLikes={user.likes} />
    </div>
  );
}
  • 客户端组件对于需要客户端交互性、状态管理或浏览器 API 的功能来说是必不可少的。
// LikeButton.js(客户端组件)

// 此组件可以处理交互
'use client'

import { useState } from 'react';

function LikeButton({ initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);

  const handleLike = () => {
    setLikes(likes + 1);
  };

  return (
    <button onClick={handleLike}>
      点赞({likes})
    </button>
  );
}
  • 推荐的做法是将客户端组件放在组件树尽可能低的位置,理想情况下使它们成为叶子组件。

客户端组件是组件树中的叶节点

引入服务器组件并不意味着它们本质上优于客户端组件,或者应该始终被优先选择。两者在一个结构良好的 React 应用程序中各有其位置。客户端组件对于创建交互式、动态用户界面至关重要,在适当的地方使用它们不仅是可接受的,而且往往是必要的。

请记住,这种新架构的目标是为构建高效的 React 应用程序提供更多工具,而不是完全替代客户端组件。在合适的地方使用服务器组件,不要犹豫在满足您需求的地方使用客户端组件。

误解 2:use client 标记一个组件为客户端组件,而 use server 标记一个组件为服务器组件

你可能会认为我们声明服务器和客户端组件的方式有一种对称性。新架构旨在支持 React 服务器组件,允讱它们仅在服务器上运行并利用服务器端渲染的好处。实际情况有些不同:

  • use client 确实标记了客户端组件的边界。
// LikeButton.js - 'use client' 让你标记哪些代码在客户端运行
'use client'

function LikeButton({ initialLikes }) {
  // 组件的其余代码
}
  • 然而,对于标记服务器组件,并没有相应的 use server 指令。在 Next.js App Router和 React 服务器组件范式中,服务器组件是默认选项。
// UserProfile.js - 默认为服务器组件

async function UserProfile({ userId }) {
  // 组件代码的其余部分
}

这种区别从根本上影响了你组织和构建 React 应用的方式。通过将服务器组件设为默认,React 鼓励采取“服务器优先”的方法,允许你在不需要额外努力的情况下利用服务器端渲染的优势。你只需要明确标记客户端组件,这有助于快速识别需要客户端交互的部分。这种方法可以带来更好的初始加载时间和减少客户端 JavaScript,优化应用的性能。

值得注意的是,确实存在一个 use server 指令,但它用于不同的目的。它用于标记 Server Action,而不是声明服务器组件。

误解 3:服务器组件总是在服务器上渲染,而客户端组件总是在浏览器中渲染

乍一看,你可能会认为服务器和客户端组件的渲染位置有一个明确的区分。传统的 React 组件通常在客户端渲染,但随着服务器和客户端组件的引入,渲染过程变得更加细致:

React 服务器组件架构中服务器和客户端组件的渲染

  • 服务器组件确实在服务器上渲染。
  • 然而,许多人没有意识到,客户端组件比较多功能。它们可以在服务器(用于初始页面加载)和客户端上渲染。
  • 关键区别在于它们的功能:客户端组件可以使用客户端特性,如状态、效果和事件处理程序,而服务器组件则不能。

仅依赖客户端 JavaScript 渲染网页会带来重大不利。用户最初会看到一个空白页面,直到他们的浏览器下载、处理并运行 JavaScript 代码,这导致了内容可见性的延迟。这种延迟会让网站感觉缓慢和无响应,特别是在网络较慢或设备性能较差的情况下。此外,它还可能阻碍搜索引擎索引,可能影响网站在搜索结果中的可见度。

为了优化这一点,你可以在服务器上为初始页面加载渲染客户端组件。这种预渲染的 HTML 允许用户立即看到内容,同时 React 在后台工作,使组件完全交互,对用户来说没有任何可见的变化。这种方法显著提高了应用程序的感知加载速度和实际性能。

误解 4:每个具有交互性的组件都需要自己的 use client 指令

你可能会认为每个具有任何客户端行为的组件都需要自己的 use client 指令。然而,事实并非如此:

  • 你只需要在文件顶部为最顶层需要客户端功能的组件添加 use client
  • 该文件中定义的所有组件自动成为客户端组件。
  • 这包括文件中的嵌套组件、实用工具和函数。

在下面的组件树中,最初是服务器组件的 FormatText.js,当导入到标有 use client 指令的客户端组件 Dashboard.js 时,会在客户端执行。(白色代表服务器组件,蓝色代表客户端组件。)

use client marking Dashboard component and children as as client components

为什么理解这一点很重要?

由于 JavaScript 模块可以在服务器和客户端组件之间共享,因此只打算用于服务器的代码可能最终在客户端运行。

考虑这个数据获取函数:

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  })
  return res.json()
}

虽然它在服务器和客户端上都能正常工作,但它包含了一个仅供服务器端使用的 API_KEY。如果这个函数被导入到客户端组件中,可能会暴露敏感信息。通过将敏感数据和逻辑,例如令牌和 API 密钥,保留在服务器上,服务器组件增强了应用程序的安全性。

Next.js 将私有环境变量(那些没有以 NEXT_PUBLIC 为前缀的)在客户端替换为一个空字符串,以防止泄露。这意呈着如果在客户端执行 getData(),它将不会按预期工作。

理解这些细微差别有助于您更安全、更有效地构建您的应用程序。通过注意您的代码运行位置,您可以防止敏感信息泄露,并确保您的组件无论是在服务器还是客户端上都能按预期工作。

误解 5:服务器组件不能嵌套在客户端组件内

你可能会认为,一旦你进入了客户端组件领域,就不能回头,因此服务器组件不能嵌套在客户端组件内。但实际上比你想象的更有弹性:

  • 虽然你不能直接在客户端组件内嵌套服务器组件,但你可以将服务器组件作为 props 传递给客户端组件。这种方式允许客户端和服务器组件的无缝整合,优化性能和互动性。
  • 一个常见的模式被称为 “children as props” 模式。这里有一个简单的例子:
// ServerComponent.js
function ServerComponent() {
  return <h1>Hello from the server</h1>;
}

// ClientComponent.js
'use client'
function ClientComponent({ children }) {
  return (
    <div>
      <p>This is a client component</p>
      {children}
    </div>
  );
}

// Usage
function App() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  );
}

这种灵活性使您能够通过将数据获取和大量计算保留在服务器上,同时仍然在需要的地方提供交互性,从而创建更高效的应用程序。这就像拥有两全其美的情况 – 服务器端操作的强大力量与客户端交互性的响应性。

React 服务器组件的引入并不意味着你需要在一夜之间彻底改变你构建 React 应用的方式。相反,将其视为你开发者工具包中的一个强大新工具,一个你可以逐渐采纳并实验的工具,以优化你的 React 应用程序。