Skip to Content

第一部分:经典 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) 的概念,这彻底改变了组件通信的思考方式。

核心规则:

  1. 服务器 -> 客户端:可以。服务器组件(RSC)可以像传递普通 props 一样,将数据(必须是可序列化的)传递给它渲染的客户端组件(CC)。
  2. 客户端 -> 服务器不可以直接通信(例如,不能将函数 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 ActionsApp Router 首选。用于表单提交、数据变更等操作,无需写 API 路由。
兄弟组件 (客户端)状态提升将共享状态放在共同父组件中管理。
跨层级/全局状态Context 或 Zustand/Redux用于客户端组件树中深度共享的状态,如主题、用户登录状态。
通过 URL 驱动数据刷新路由 (<Link>, router, searchParams)客户端改变 URL,触发服务器组件重新获取数据。非常适合筛选、分页等场景。
布局与内容分离children Prop (插槽)客户端组件提供交互外壳,服务器组件作为 children 填充内容。实现了关注点分离。

理解并熟练运用这些模式,特别是区分服务器和客户端组件的边界和通信规则,是高效开发现代 Next.js 应用的关键。

Last updated on