理解服务器/客户端组件
在Next.js App Router中,所有组件默认都是服务器组件。 然而,有时你需要客户端功能(例如useState、onClick等)。在这种情况下应该怎么做?
要使用客户端组件,请在文件顶部添加'use client'
指令。
'use client'
export const ClientComponent = () => {
return (
<div>
<h1>Client Component</h1>
</div>
);
};
这会从该组件创建一个客户端边界,组件中导入的所有组件和模块文件都在客户端运行。 (这里重要的不是组件的渲染层次结构,而是导入关系。)
因此,没有'use client'
的组件也会自动转换为客户端组件。
这种情况怎么样?让我们在客户端组件内声明一个服务器组件。
import React from 'react';
export const ServerComponent = async () => {
const data = await getData();
return <div>{JSON.stringify(data)}</div>;
};
async function getData() {
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const json = await res.json();
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return json;
}
export default ServerComponent;
'use client'
import ServerComponent from './ServerComponent'
export const ClientComponent = () => {
return (
<div>
<h1>Client Component</h1>
<ServerComponent />
</div>
)
}
你会看到带有以下警告的错误日志:
当同时使用客户端组件和服务器组件时,有重要的规则:
- ✅ 服务器组件下的客户端组件(可行)
- ✅ 客户端组件下的客户端组件(可行)
- ❌ 客户端组件下的服务器组件(不可行)
🔧 解决方案:使用Props模式
那么如何在客户端组件内使用服务器组件?答案很简单:通过props传递。
// 有效的模式
'use client'
export const ClientComponent = ({ children }) => {
return (
<div>
<h1>Client Component</h1>
{children}
</div>
);
};
const App = () => {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
为什么它这样工作?
关键是客户端边界由导入位置决定,而不是组件的父子关系。
在上面的例子中,ServerComponent
由App导入,App是一个服务器组件,所以它保持为服务器组件。
ClientComponent
只知道在哪里渲染它作为props接收的children,但不知道会有什么组件。
这种模式允许你使用服务器组件,即使它们被Layout中用use client声明的providers包裹。
深入探讨
服务器组件的运行方式与传统的服务器端渲染(SSR)完全不同。
与SSR在服务器上生成完整HTML不同,服务器组件生成一种称为React Server Components(RSC)Payload的特殊JSON类流。
这个RSC Payload包括组件树结构、数据、HTML内容等,服务器将基本数据类型如字符串、数字、数组和内置对象如React元素、Date对象、Map、Set等序列化,以包含在这个payload中。 另一方面,函数、类实例、闭包和事件监听器等不能被序列化。
因此,你不能从服务器组件向客户端组件传递不能序列化的值作为props
!
在渲染服务器组件时,如果遇到客户端组件,该部分会被标记为特殊的占位符。 这个占位符包括客户端组件的引用和props信息,允许客户端准确地渲染该组件。
客户端将从服务器接收的RSC payload转换为React可以理解的形式,并对从服务器接收的内容进行hydration。 此时,在标记为占位符的部分渲染相应的客户端组件。
参考