Next.js 应用程序路由器将你的应用程序分为服务器组件和客户端组件。服务器组件在服务器端运行,用于保护敏感数据;客户端组件则在浏览器中运行,负责处理交互逻辑。挑战在于如何在保持各自环境规则不变的前提下,在这些组件之间共享数据和用户界面。
本指南会向你展示如何在 Next.js 中实现服务器组件与客户端组件之间的数据及组件共享。你将学习到组合模式、属性传递规则,以及何时使用哪种方法。
目录
什么是服务器组件和客户端组件?
在 Next.js 应用程序路由器中,所有组件默认都属于服务器组件。服务器组件仅在服务器端运行,它们可以从数据库中获取数据、使用 API 密钥,并将敏感逻辑置于浏览器之外;因此,它们不会向客户端发送 JavaScript 代码,从而减少了应用程序包的大小。
客户端组件既在服务器端运行(用于生成初始 HTML 内容),也在客户端运行(负责处理交互操作)。你需要在文件顶部使用 `"use client"` 指令来标记这些组件;它们可以使用 `useState`、`useEffect` 等函数,以及 `localStorage`、`window` 等浏览器 API。
关键规则是:**服务器组件可以导入并渲染客户端组件,但客户端组件不能直接导入服务器组件**。客户端组件只能通过属性(如 `children`)来接收服务器组件。
先决条件
在开始学习之前,请确保你已经具备以下条件:
-
对 React 有基本了解(包括组件、属性和钩子等概念)
-
拥有一个使用 Next.js 应用程序路由器的项目(Next.js 13 及更高版本)
-
已安装 Node.js(建议使用 18 及更高版本)
如果你还没有 Next.js 项目,可以使用以下命令创建一个:
npx create-next-app@latest my-app
如何通过属性在服务器和客户端之间传递数据
在服务器组件和客户端组件之间共享数据的最简单方法就是通过属性来传递。服务器组件负责获取数据,而客户端组件则接收这些数据并处理交互逻辑。
下面是一个基本的示例:一个页面(服务器组件)会获取一篇帖子的信息,并将点赞数传递给 LikeButton(客户端组件):
// app/post/[id]/page.jsx (服务器组件)
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';
export default async function PostPage({ params }) {
const { id } = await params;
const post = await getPost(id);
return (
{post.title}
{post.content}
);
}
// app/ui/like-button.jsx (客户端组件)
'use client';
import { useState } from 'react';
export default function LikeButton({ likes,postId }) {
const [count, setCount] = useState(likes);
const handleLike = () => {
setCount((c) => c + 1);
// 调用 API 或服务器端接口来保存数据
};
return (
);
}
服务器组件在服务器端获取数据,客户端组件则接收这些原始数据(如 likes、postId),并负责管理状态和处理交互事件。这种模式使得数据获取工作在服务器端完成,而交互逻辑则在客户端实现。
如何将服务器组件作为子元素传递给客户端组件
你可以将一个服务器组件作为 children 属性(或任何其他属性)传递给客户端组件。服务器组件仍然会在服务器端进行渲染,而客户端组件接收的是已经渲染好的结果,而不是组件的源代码。
当你需要让客户端组件来包裹或控制由服务器端生成的内容的布局时,这种做法非常有用。例如,一个用于显示从服务器获取的数据的模态框:
// app/ui/modal.jsx (客户端组件)
'use client';
import { useState } from 'react';
export default function Modal({ children }) {
const [isOpen, setIsOpen] = useState(false);
return (
{isOpen && (
{children}
)}
);
}
// app/cart/page.jsx (服务器组件)
import Modal from '@/app/ui/modal';
import Cart from '@/app/ui/cart';
export default function CartPage() {
return (
);
}
// app/ui/cart.jsx(服务器组件,不使用“use client”)
import { getCart } from '@/lib/cart';
export default async function Cart() {
const items = await getCart();
return (
{items.map((item) => (
- {item.name}
))}
);
}
Cart是一个服务器组件,用于获取购物车数据。它会被作为children参数传递给Modal这个客户端组件。服务器会先渲染Cart组件,然后RSC响应中会包含渲染后的结果。客户端接收到这些数据后,会在模态框中显示它们。需要注意的是,购物车数据从不会在客户端上进行处理。
你也可以使用同样的模式来处理带有名称的属性(即插槽):
// app/ui/tabs.jsx(客户端组件)
'use client';
import { useState } from 'react';
export default function Tabs({ tabs, children }) {
const [activeIndex, setActiveIndex] = useState(0);
return (
{tabs.map((tab, i) => (
))}
{children[activeIndex]}
);
}
// app/dashboard/page.jsx(服务器组件)
import Tabs from '@/app/ui/tabs';
import Overview from '@/app/ui/overview';
import Analytics from '@/app/ui/analytics';
export default function DashboardPage() {
const tabs = [
{ id: 'overview', label: '概览' },
{ id: 'analytics', label: '分析数据' },
];
return (
);
}
Overview和Analytics也可以作为服务器组件来获取各自的数据。这些组件的内容会在服务器端被渲染出来,然后客户端会接收到已经预渲染好的结果。
服务器与客户端之间允许传递哪些属性?
从服务器组件传递给客户端组件的属性必须是可序列化的。React会将这些属性序列化到RSC响应中,从而能够将它们发送给客户端。
允许的类型
-
字符串、数字、布尔值
-
null和undefined -
普通对象(不能包含函数或类实例)
-
可序列化值的数组
-
JSX代码(可以用作服务器组件的子元素或其他属性)
-
服务器动作函数(使用
"use server"声明的函数)
不允许的类型
-
函数(服务器动作函数除外)
-
Date对象 -
类实例
-
符号
-
Map、Set、WeakMap、WeakSet -
具有自定义原型的对象
-
缓冲区、
ArrayBuffer以及类型化数组
如果需要传递一个Date》对象,请先将其转换为字符串或数字:
<ClientComponentcreatedAt={post.createdAt.toISOString()} />
如果使用MongoDB,需将ObjectId转换为字符串:
<PostThread userId={user._id.toString()] />
将服务器操作作为属性传递
服务器操作是带有"use server"标记的异步函数。你可以将它们作为属性传递给客户端组件。这些操作是按引用进行序列化的,而不是按值进行序列化。
// app/actions/post.js
'use server';
export async function likePost(postId) {
// 更新数据库
revalidatePath `/post/${postId}`);
}
app/post/[id]/page.jsx (服务器组件)
import LikeButton from '@/app/ui/like-button';
import { likePost } from '@/app/actions/post';
export default async function PostPage({ params }) {
const post = await getPost((await params).id);
return <LikeButton likes={postlikes} postId={post.id} onLike={likePost} />;
}
app/ui/like-button.jsx (客户端组件)
'use client';
export default function LikeButton({ likes, postId, onLike }) {
const handleClick = () => {
onLike(postId);
};
return <button onClick={handleClick}>{likes} likes<>/button>;
}
你也可以为这些参数绑定具体的值:
<LikeButton onLike={likePost.bind(null, post.id)} />
如何利用React Context和React.cache共享数据
React Context在服务器组件中是无法使用的。要想在服务器组件与客户端组件之间共享数据,你可以将客户端组件的Context提供者与React.cache结合使用,以实现服务器端的缓存功能。
创建一个被缓存的获取数据的函数:
lib/user.js
import { cache } from 'react';
export const getUser = cache(async () => {
const res = await fetch('https://api.example.com/user');
return res.json();
});
创建一个能够接收Promise并将其存储在Context中的提供者:
appproviders/user-provider.jsx
'use client';
import { createContext } from 'react';
export const UserContext = createContext(null);
export default function UserProvider({ children, userPromise }) {
return (
);
}
在根布局中,可以直接传递这个Promise,而无需等待其执行结果:
app/layout.jsx
import UserProvider from '@/appproviders/user-provider';
import { getUser } from '@/lib/user';
export default function RootLayout({ children }) {
const userPromise = getUser();
return (
);
}
客户端组件会使用use()来解析这些Promise:
// app/ui/profile.jsx
'use client';
import { use, useContext } from 'react';
import { UserContext } from '@/appproviders/user-provider';
export default function Profile() {
const userPromise = useContext(UserContext);
if (!userPromise) {
throw new Error('Profile必须在与UserProvider一起使用时才能使用');
}
const user = use(userPromise);
return 欢迎,{user.name}
;
}
为了处理组件的加载状态,需要用Suspense包装这些组件:
// app/dashboard/page.jsx
import { Suspense } from 'react';
import Profile from '@/app/ui/profile';
export default function DashboardPage() {
return (
正在加载用户信息...

