第一部分:经典 React 通信模式(在 Next.js 中同样适用)
这些模式主要应用于 客户端组件(Client Components) 之间,因为它们拥有状态(state)和生命周期钩子(hooks)。
1. 父组件 -> 子组件:Props
这是最基本、最直接的通信方式。父组件通过 props 将数据或函数单向传递给子组件。
特点:
- 单向数据流,清晰可控。
- 简单、高效。
示例:
// app/components/ParentComponent.tsx
'use client'; // 这是一个客户端组件
import ChildComponent from './ChildComponent';
export default function ParentComponent() {
const message = "你好,我是父组件传递的消息!";
return (
<div>
<h1>父组件</h1>
<ChildComponent message={message} />
</div>
);
}
// app/components/ChildComponent.tsx
// 'use client' 指令可以不写,如果它只接收 props 而没有交互,
// 但为了清晰,这里标明它在客户端组件树中
'use client';
interface ChildProps {
message: string;
}
export default function ChildComponent({ message }: ChildProps) {
return (
<div style={{ border: '1px solid gray', padding: '10px', marginTop: '10px' }}>
<h2>子组件</h2>
<p>接收到的消息: {message}</p>
</div>
);
}
2. 子组件 -> 父组件:回调函数(Callback Functions)
子组件无法直接修改父组件的状态。为了实现“向上”通信,父组件可以传递一个函数作为 prop 给子组件,子组件在特定时机(如用户点击)调用这个函数,从而将数据或事件通知给父组件。
特点:
- 维持了单向数据流的原则。
- 是处理子组件事件的标准模式。
示例:
// app/components/ParentWithCallback.tsx
'use client';
import { useState } from 'react';
import ChildWithButton from './ChildWithButton';
export default function ParentWithCallback() {
const [textFromChild, setTextFromChild] = useState("等待子组件的消息...");
// 这个函数将被传递给子组件
const handleChildClick = (newText: string) => {
setTextFromChild(newText);
};
return (
<div>
<h1>父组件</h1>
<p>来自子组件的消息: {textFromChild}</p>
<ChildWithButton onButtonClick={handleChildClick} />
</div>
);
}
// app/components/ChildWithButton.tsx
'use client';
interface ChildButtonProps {
onButtonClick: (message: string) => void;
}
export default function ChildWithButton({ onButtonClick }: ChildButtonProps) {
return (
<div style={{ border: '1px solid blue', padding: '10px', marginTop: '10px' }}>
<h2>子组件</h2>
<button onClick={() => onButtonClick("子组件的按钮被点击了!")}>
点击我,通知父组件
</button>
</div>
);
}
3. 兄弟组件通信:状态提升(Lifting State Up)
兄弟组件之间不能直接通信。正确的做法是将它们共享的状态提升到它们最近的共同父组件中。然后,父组件通过 props 将状态和修改状态的回调函数分别传递给需要的兄弟组件。
特点:
- 保持数据源唯一,避免数据不一致。
- 使组件结构更清晰。
- 缺点是可能导致“Prop Drilling”(属性逐层传递)。
示例: 一个输入框(SiblingA)的内内容同步显示在另一个文本区域(SiblingB)。
// app/page.tsx (或者任何父组件)
'use client';
import { useState } from 'react';
import SiblingA from './components/SiblingA';
import SiblingB from './components/SiblingB';
export default function CommonParent() {
const [sharedText, setSharedText] = useState('');
return (
<div>
<h1>共同父组件</h1>
<SiblingA text={sharedText} onTextChange={setSharedText} />
<SiblingB text={sharedText} />
</div>
);
}
// app/components/SiblingA.tsx
'use client';
interface SiblingAProps {
text: string;
onTextChange: (newText: string) => void;
}
export default function SiblingA({ text, onTextChange }: SiblingAProps) {
return (
<input
value={text}
onChange={(e) => onTextChange(e.target.value)}
placeholder="在这里输入..."
/>
);
}
// app/components/SiblingB.tsx
'use client';
interface SiblingBProps {
text: string;
}
export default function SiblingB({ text }: SiblingBProps) {
return <p>同步显示的内容: {text}</p>;
}
4. 跨层级/全局通信:Context API 或状态管理库
当组件层级很深,使用“状态提升”会导致严重的 Prop Drilling 时,就需要更强大的工具。
a. React Context API React 内置的解决方案,用于在组件树中深度共享“全局”数据,而无需手动一层层传递 props。
- 适用场景:主题(暗/亮模式)、用户认证信息、地区设置等不频繁更新的全局数据。
- 使用方法:创建
Context
-> 使用Provider
包裹组件树并提供value
-> 在任何子组件中使用useContext
hook 来消费数据。
b. 状态管理库 (Zustand, Redux, Jotai 等) 当应用状态变得非常复杂、更新频繁、且涉及异步操作时,专业的状体管理库是更好的选择。
- Zustand:轻量、简单,基于 hooks,上手快,是目前社区非常推崇的选择。
- Redux Toolkit:功能强大、生态成熟,适合大型、复杂、需要严格数据流管理的应用。
第二部分:现代 Next.js App Router 模式
App Router 引入了 服务器组件(RSC) 和 客户端组件(CC) 的概念,这彻底改变了组件通信的思考方式。
核心规则:
- 服务器 -> 客户端:可以。服务器组件(RSC)可以像传递普通 props 一样,将数据(必须是可序列化的)传递给它渲染的客户端组件(CC)。
- 客户端 -> 服务器:不可以直接通信(例如,不能将函数 prop 从 CC 传给 RSC)。客户端到服务器的通信需要通过特定的机制。
5. 服务器组件 -> 客户端组件:Props (单向数据流)
这是最常见的模式。服务器组件在服务端获取数据(如从数据库、API),然后将这些数据作为 props 传递给需要交互的客户端组件。
// app/page.tsx (Server Component by default)
import ClientUserCard from './components/ClientUserCard';
async function getUserData() {
const res = await fetch('https://api.example.com/user/1');
return res.json();
}
export default async function Page() {
const userData = await getUserData(); // 在服务器上获取数据
// 将数据作为 props 传给客户端组件
return (
<main>
<h1>用户主页 (服务器组件)</h1>
<ClientUserCard user={userData} />
</main>
);
}
// app/components/ClientUserCard.tsx
'use client'; // 标记为客户端组件
interface User {
name: string;
email: string;
}
export default function ClientUserCard({ user }: { user: User }) {
const handleClick = () => {
alert(`正在联系 ${user.name}`);
};
return (
<div onClick={handleClick} style={{ cursor: 'pointer' }}>
<h2>{user.name} (客户端组件)</h2>
<p>{user.email}</p>
</div>
);
}
6. 客户端 -> 服务器:Server Actions
这是 App Router 的革命性功能,是实现从客户端向服务器发送数据和执行操作的首选方式。
特点:
- 无需手动创建 API 路由。
- 函数直接定义在服务器端(可以在服务器组件中,或单独文件中),但可以在客户端组件的表单
action
或事件处理函数中调用。 - Next.js 自动处理请求、响应和页面的数据重新验证。
示例: 一个客户端的表单,提交后由服务器组件中的 Server Action 处理。
// app/actions.ts (推荐将 Server Actions 放在单独文件中)
'use server'; // 标记整个文件中的函数都是 Server Actions
import { revalidatePath } from 'next/cache';
// 这是一个 Server Action
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
// 模拟保存到数据库
console.log('在服务器端创建帖子:', title);
// await db.post.create({ data: { title } });
// 清除缓存并重新获取数据,以更新页面
revalidatePath('/');
}
// app/components/PostForm.tsx
'use client';
import { createPost } from '../actions';
export default function PostForm() {
return (
// 直接将 Server Action 绑定到 form 的 action 属性
<form action={createPost}>
<input type="text" name="title" placeholder="帖子标题" required />
<button type="submit">创建帖子</button>
</form>
);
}
7. 通过 URL 状态通信 (路由)
这是一种强大且被低估的通信方式,尤其适用于服务器组件。通过改变 URL(路径或查询参数),可以触发服务器组件重新渲染并获取新数据。
特点:
- 状态是可分享和可收藏的(URL 可以被复制粘贴)。
- 完美契合服务器组件的数据获取模型。
- 客户端和服务器组件都能访问和响应 URL 的变化。
示例:
一个商品列表页面,客户端的筛选器组件通过改变 URL 的查询参数 (?category=...
) 来通知服务器组件重新获取和渲染不同类别的商品。
// app/products/page.tsx (Server Component)
import Link from 'next/link';
async function getProducts(category?: string) {
let url = 'https://api.example.com/products';
if (category) {
url += `?category=${category}`;
}
const res = await fetch(url);
return res.json();
}
// searchParams 会由 Next.js 自动传入
export default async function ProductsPage({ searchParams }: { searchParams: { category?: string } }) {
const products = await getProducts(searchParams.category);
return (
<div>
<h1>商品列表</h1>
{/* 这是一个客户端组件,用于改变 URL */}
<CategoryFilter currentCategory={searchParams.category} />
<ul>
{products.map((p: any) => <li key={p.id}>{p.name}</li>)}
</ul>
</div>
);
}
// app/products/CategoryFilter.tsx (Client Component)
'use client';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
export default function CategoryFilter({ currentCategory }: { currentCategory?: string }) {
const router = useRouter();
const pathname = usePathname();
const handleCategoryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newCategory = e.target.value;
if (newCategory) {
router.push(`${pathname}?category=${newCategory}`);
} else {
router.push(pathname); // 清除筛选
}
};
return (
<select onChange={handleCategoryChange} value={currentCategory || ''}>
<option value="">所有分类</option>
<option value="electronics">电子产品</option>
<option value="books">图书</option>
</select>
);
}
8. 组件组合与 children
Prop (插槽模式)
这是一种优雅的设计模式,特别适用于将 服务器组件 “嵌入”到 客户端组件 的布局中,同时保持它们的身份。
父组件(通常是客户端)定义了布局和交互,但将内容的渲染权交给了 children
prop。这样,你可以传递一个在服务器上渲染好的、包含数据内容的服务器组件作为 children
。
示例: 一个带交互的客户端模态框(Modal),但其内容是一个在服务器上获取并渲染好的用户信息组件。
// app/components/ClientModal.tsx
'use-client';
import { useState } from 'react';
export default function ClientModal({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
if (!isOpen) {
return <button onClick={() => setIsOpen(true)}>打开模态框</button>;
}
return (
<div>
{/* 模态框的遮罩和关闭按钮等交互逻辑 */}
<button onClick={() => setIsOpen(false)}>关闭</button>
{/* 内容区域,这里渲染的是从父组件传来的 children */}
<div>{children}</div>
</div>
);
}
// app/page.tsx (Server Component)
import ClientModal from './components/ClientModal';
import UserDetails from './components/UserDetails'; // 这是一个服务器组件
export default async function Page() {
return (
<div>
<h1>主页</h1>
<ClientModal>
{/*
UserDetails 是一个服务器组件,它会在这里被渲染。
它可以在自己的文件里 async/await 获取数据。
ClientModal 只负责提供 "插槽" 和交互,不知道内容是什么。
*/}
<UserDetails userId="123" />
</ClientModal>
</div>
);
}
// app/components/UserDetails.tsx (Server Component)
async function getUser(id: string) { /* ...获取数据... */ return { name: 'Alice' }; }
export default async function UserDetails({ userId }: { userId: string }) {
const user = await getUser(userId);
return <div>用户姓名: {user.name}</div>; // 这部分在服务器上渲染
}
总结与选择建议
通信场景 | 推荐方法 | 说明 |
---|---|---|
父 -> 子 (客户端) | Props | 最基础、最直接的方式。 |
服务器 -> 客户端 | Props | 服务器组件获取数据后,通过 props 传递给客户端组件。数据必须可序列化。 |
子 -> 父 (客户端) | 回调函数 | 子组件调用父组件传递的函数,实现向上通信。 |
客户端 -> 服务器 | Server Actions | App Router 首选。用于表单提交、数据变更等操作,无需写 API 路由。 |
兄弟组件 (客户端) | 状态提升 | 将共享状态放在共同父组件中管理。 |
跨层级/全局状态 | Context 或 Zustand/Redux | 用于客户端组件树中深度共享的状态,如主题、用户登录状态。 |
通过 URL 驱动数据刷新 | 路由 (<Link> , router , searchParams ) | 客户端改变 URL,触发服务器组件重新获取数据。非常适合筛选、分页等场景。 |
布局与内容分离 | children Prop (插槽) | 客户端组件提供交互外壳,服务器组件作为 children 填充内容。实现了关注点分离。 |
理解并熟练运用这些模式,特别是区分服务器和客户端组件的边界和通信规则,是高效开发现代 Next.js 应用的关键。