《Go 后端开发者学 React:组件通信与插槽》

文章来源声明: 原文作者:Kevin666; 来源站点:掘金; 原文链接:https://juejin.cn/post/7705376708413587483; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

适合有后端经验、初学 React 的开发者快速建立组件设计心智。内容从基础通信到插槽模式层层递进,选择表和实战案例便于直接用于公共组件封装。

React 组件通信与插槽完全指南:从 Props、Children 到组件设计实战 ------------------------------------------

React + TypeScript 学习笔记 · 组件通信 · 组件复用 · 由浅入深

前言

学习 React 时,经常会遇到几个问题:

  • 父组件如何给子组件传递数据?
  • 子组件如何通知父组件修改数据?
  • 兄弟组件之间如何共享数据?
  • React 有没有类似 Vue 的 slot 插槽?
  • props、children、Context 分别什么时候使用?

其实,这些问题都属于 React 的两个核心知识点:

组件通信:解决组件之间如何传递数据和事件。

组件插槽:解决组件内部的内容如何由外部决定。

本文使用 React 函数组件和 TypeScript,从最简单的例子逐步深入。

一、先理解 React 组件是什么

在 React 中,组件可以简单理解为:一个返回 JSX 的函数。

例如:

function UserCard() {
  return (
    <div>
      <h3>张三</h3>
      <p>年龄:25</p>
    </div>
  );
}

然后在另一个组件中使用:

function App() {
  return (
    <div>
      <UserCard />
      <UserCard />
    </div>
  );
}

这样就相当于复用了两次 UserCard 组件。

但有个问题:两个组件都显示张三。

如果希望不同组件显示不同用户,就需要组件通信。

二、父传子:Props(最基础)

Props 是 React 中最常用的组件通信方式。

一句话理解:

父组件通过属性给子组件传递数据,子组件通过 props 接收。

2.1 最简单的例子

子组件:

// UserCard.tsx

interface UserCardProps {
  name: string;
  age: number;
}

function UserCard(props: UserCardProps) {
  return (
    <div>
      <h3>{props.name}</h3>
      <p>年龄:{props.age}</p>
    </div>
  );
}

export default UserCard;

父组件:

// App.tsx

import UserCard from "./UserCard";

function App() {
  return (
    <div>
      <UserCard name="张三" age={25} />
      <UserCard name="李四" age={30} />
    </div>
  );
}

效果:

image.png核心关系:

image.png

需要注意:

  • name 和 age 是父组件传入的属性。
  • UserCardProps 定义属性的 TypeScript 类型。
  • 子组件只能读取 props,不应该直接修改 props。

2.2 Props 解构写法

上面的写法:

function UserCard(props: UserCardProps) {
  return <h3>{props.name}</h3>;
}

实际开发中更常使用解构:

function UserCard({ name, age }: UserCardProps) {
  return (
    <div>
      <h3>{name}</h3>
      <p>年龄:{age}</p>
    </div>
  );
}

这里的:

{ name, age }: UserCardProps

是 JavaScript 的对象解构,加上 TypeScript 类型声明。

2.3 Props 可以传递什么?

不仅能传递字符串、数字,还可以传递对象、数组、函数。

interface User {
  id: number;
  name: string;
}

interface UserCardProps {
  user: User;
  tags: string[];
  active: boolean;
}

function UserCard({ user, tags, active }: UserCardProps) {
  return (
    <div>
      <h3>{user.name}</h3>

      <p>{active ? "在线" : "离线"}</p>

      {tags.map(tag => (
        <span key={tag}>{tag}</span>
      ))}
    </div>
  );
}

父组件:

<UserCard
  user={{ id: 1, name: "张三" }}
  tags={["管理员", "开发者"]}
  active={true}
/>

小结:Props 负责把数据从父组件传给子组件,是 React 最基础的通信方式。

三、子传父:回调函数(重点)

现在假设有一个需求:

父组件维护任务列表,子组件展示任务。

用户点击子组件中的“删除”按钮,希望父组件删除对应任务。

问题是:子组件怎么通知父组件?

React 通常使用回调函数实现。

核心思想:

父组件把函数传给子组件,子组件调用这个函数,把信息传回父组件。

3.1 基础例子

子组件:

interface ChildProps {
  onMessage: (message: string) => void;
}

function Child({ onMessage }: ChildProps) {
  return (
    <button onClick={() => onMessage("你好,父组件!")}>
      发送消息
    </button>
  );
}

父组件:

import { useState } from "react";

function Parent() {
  const [message, setMessage] = useState("");

  const handleMessage = (msg: string) => {
    setMessage(msg);
  };

  return (
    <div>
      <h3>收到消息:{message}</h3>

      <Child onMessage={handleMessage} />
    </div>
  );
}

交互演示

父组件 Parent

收到消息:暂无消息

子组件 Child

点击向父组件发送消息

整个流程:

父组件 Parent
   │
   │ 传递 handleMessage 函数
   ▼
子组件 Child
   │
   │ 用户点击按钮
   │
   │ onMessage("你好")
   ▼
父组件 handleMessage()
   │
   │ setMessage()
   ▼
父组件重新渲染

这里需要理解:

onMessage: (message: string) => void;

表示 onMessage 是一个函数类型,接收 string 参数,返回 void。

3.2 实战:子组件删除父组件中的任务

这是一个很典型的业务场景。

import { useState } from "react";

interface Task {
  id: number;
  name: string;
}

interface TaskItemProps {
  task: Task;
  onDelete: (id: number) => void;
}

// 子组件
function TaskItem({ task, onDelete }: TaskItemProps) {
  return (
    <div>
      <span>{task.name}</span>

      <button onClick={() => onDelete(task.id)}>
        删除
      </button>
    </div>
  );
}

// 父组件
function TaskList() {
  const [tasks, setTasks] = useState<Task[]>([
    { id: 1, name: "视频回查" },
    { id: 2, name: "文章回扫" }
  ]);

  const handleDelete = (id: number) => {
    setTasks(prev => prev.filter(task => task.id !== id));
  };

  return (
    <div>
      {tasks.map(task => (
        <TaskItem
          key={task.id}
          task={task}
          onDelete={handleDelete}
        />
      ))}
    </div>
  );
}

这里同时存在两种通信:

  • task={task}:父组件向子组件传数据。
  • onDelete={handleDelete}:父组件向子组件传回调,子组件通过调用它通知父组件。

子传父本质上并不是子组件直接修改父组件的数据,而是子组件调用父组件提供的函数。

四、兄弟组件通信:状态提升

假设有两个组件:

          Parent
         /      \
     Search     UserList

  • Search:输入搜索关键词。
  • UserList:根据关键词展示用户。

它们是兄弟组件,应该怎么通信?

最常见的方案是:把共享状态放在共同的父组件中。

这叫做 Lifting State Up(状态提升)。

4.1 完整示例

import { useState } from "react";

interface SearchProps {
  keyword: string;
  onKeywordChange: (value: string) => void;
}

function Search({ keyword, onKeywordChange }: SearchProps) {
  return (
    <input
      value={keyword}
      onChange={e => onKeywordChange(e.target.value)}
      placeholder="搜索用户"
    />
  );
}

interface UserListProps {
  keyword: string;
}

function UserList({ keyword }: UserListProps) {
  const users = ["张三", "李四", "王五"];

  return (
    <ul>
      {users
        .filter(name => name.includes(keyword))
        .map(name => (
          <li key={name}>{name}</li>
        ))}
    </ul>
  );
}

function Parent() {
  const [keyword, setKeyword] = useState("");

  return (
    <div>
      <Search
        keyword={keyword}
        onKeywordChange={setKeyword}
      />

      <UserList keyword={keyword} />
    </div>
  );
}

交互演示

image.png

核心思路:

Search 输入关键词
        │
        ▼
Parent 更新 keyword
        │
        ▼
UserList 接收新 keyword
        │
        ▼
列表重新渲染

记住:兄弟组件通常不直接互相通信,而是通过共同父组件共享状态。

五、React 插槽:children(重点)

前面讲的都是组件之间传递数据。

现在换一个需求。

假设你要开发一个公共弹窗组件 Modal,但是每个弹窗展示的内容都不一样。

例如:

  • 删除弹窗:展示“确定删除吗?”
  • 用户弹窗:展示用户信息。
  • 任务弹窗:展示任务执行详情。

难道每个弹窗都要重新写一遍吗?

不需要。

这就引出了**插槽(Slot)**的概念。

5.1 什么是插槽?

一句话理解:

插槽就是组件预留一个位置,允许外部组件把内容放进来。

React 没有完全对应 Vue slot 的内置语法,最常见的实现方式是 props.children。

先看一个例子:

import type { ReactNode } from "react";

interface CardProps {
  children: ReactNode;
}

function Card({ children }: CardProps) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

使用:

function App() {
  return (
    <Card>
      <h2>用户信息</h2>
      <p>姓名:张三</p>
      <p>年龄:25</p>
    </Card>
  );
}

渲染效果:

用户信息

image.png

这里:

<Card>
  <h2>用户信息</h2>
  <p>姓名:张三</p>
</Card>

<Card> 标签中间的内容,会作为 children 传递给 Card 组件。

相当于:

<Card
  children={
    <>
      <h2>用户信息</h2>
      <p>姓名:张三</p>
    </>
  }
/>

其中 <>...</> 是 React Fragment,用来组合多个元素,不额外创建 DOM 节点。

插槽原理

image.png

Card 组件

children 插入的位置

<UserInfo />

你可以把 children 理解成:

父组件把一块 JSX 内容交给子组件,子组件决定这块内容放在哪里。

5.2 children 可以传入什么?

children 不局限于 HTML 标签,也可以是字符串、数字、组件或多个 JSX 元素。

例如:

<Card>你好</Card>

<Card>
  <UserInfo />
</Card>

<Card>
  <h2>标题</h2>
  <TaskList />
  <button>提交</button>
</Card>

在 TypeScript 中,通常使用:

import type { ReactNode } from "react";

interface CardProps {
  children: ReactNode;
}

ReactNode 是能够作为 React 渲染内容的类型。

5.3 React 插槽和 Vue 插槽对比

如果以后看到 Vue 的代码,可以这样理解:

Vue

<!-- Card.vue -->
<template>
  <div>
    <slot />
  </div>
</template>

使用:

<Card>
  <h2>用户信息</h2>
</Card>

React

function Card({ children }) {
  return (
    <div>
      {children}
    </div>
  );
}

使用:

<Card>
  <h2>用户信息</h2>
</Card>

两者思想相似:

Vue 使用 <slot />,React 通常使用 {children}。

六、具名插槽:多个位置如何传入不同内容?

children 适合一个主要内容区域。

但有时候,一个组件需要多个插槽。

例如一个页面布局:

image.png

如果只有 children,怎么区分 Header、Sidebar 和 Footer?

React 常见的解决方式是:使用普通 props 接收不同位置的 ReactNode。

6.1 自定义多个插槽

import type { ReactNode } from "react";

interface LayoutProps {
  header: ReactNode;
  sidebar: ReactNode;
  children: ReactNode;
  footer?: ReactNode;
}

function Layout({
  header,
  sidebar,
  children,
  footer
}: LayoutProps) {
  return (
    <div>
      <header>{header}</header>

      <div className="layout-body">
        <aside>{sidebar}</aside>
        <main>{children}</main>
      </div>

      <footer>{footer}</footer>
    </div>
  );
}

使用:

function App() {
  return (
    <Layout
      header={<h1>任务管理平台</h1>}
      sidebar={<TaskMenu />}
      footer={<p>Copyright 2026</p>}
    >
      <TaskList />
    </Layout>
  );
}

这里的:

header={<h1>任务管理平台</h1>}

就是把一段 JSX 作为属性传递给组件。

同理:

sidebar={<TaskMenu />}

表示把 TaskMenu 组件的 React 元素传给 sidebar。

6.2 什么时候使用 children,什么时候使用具名插槽?

场景推荐方式
卡片主要内容`children`
弹窗主体`children`
顶部标题`title` 或 `header`
底部操作按钮`footer`
侧边栏`sidebar`
多个可定制区域多个 `ReactNode` props

核心:React 的具名插槽通常就是把 JSX 当作 props 传递。

七、进阶插槽:Render Props

你前面学习过 renderItem,其实它就是 Render Props 思想的典型应用。

这个概念的难点就一句话:普通插槽传的是 “现成的内容”,Render Props 传的是 “一个函数”—— 数据还在子组件手里,但怎么画由你(父组件)决定。

普通插槽:

<Card>
  <h2>固定内容</h2>
</Card>

这里传入的是现成的 JSX 内容。

但如果子组件希望把内部的数据传给外部,让外部决定怎么渲染呢?

这时可以使用 Render Props。

7.1 最简单的 Render Props

import type { ReactNode } from "react";

interface User {
  id: number;
  name: string;
}

interface UserProviderProps {
  render: (user: User) => ReactNode;
}

function UserProvider({ render }: UserProviderProps) {
  const user: User = {
    id: 1,
    name: "张三"
  };

  return <div>{render(user)}</div>;
}

父组件:

function App() {
  return (
    <UserProvider
      render={(user) => (
        <h2>用户名:{user.name}</h2>
      )}
    />
  );
}

关键是:

render={(user) => (
  <h2>{user.name}</h2>
)}

这次传入的不是 JSX,而是一个返回 JSX 的函数。

子组件内部执行:

render(user)

把自己的数据传给父组件提供的渲染函数。

7.2 Render Props 和普通 children 的区别

对比childrenRender Props
传入内容JSX 元素函数
能否接收子组件提供的数据不能直接通过该元素接收可以
典型写法`内容``render={(item) => ...}`
适用场景布局、容器列表渲染、数据驱动的内容定制

7.3 renderItem 实战

例如自己封装一个通用列表:

import type { ReactNode } from "react";

interface ListProps<T extends { id: number }> {
  data: T[];
  renderItem: (item: T, index: number) => ReactNode;
}

function MyList<T extends { id: number }>({
  data,
  renderItem
}: ListProps<T>) {
  return (
    <div>
      {data.map((item, index) => (
        <div key={item.id}>
          {renderItem(item, index)}
        </div>
      ))}
    </div>
  );
}

调用:

const tasks = [
  { id: 1, name: "视频回查" },
  { id: 2, name: "文章回扫" }
];

<MyList
  data={tasks}
  renderItem={(item, index) => (
    <div>
      {index + 1}. {item.name}
    </div>
  )}
/>

可以理解为:

data.map((item, index) => renderItem(item, index))

这就是 Render Props 最核心的思想:组件提供数据,外部提供渲染方式。

八、跨层级组件通信:Context

前面介绍的 Props 很适合父子组件之间通信。

但是假设组件结构是这样的:

App
 └── Page
      └── Layout
           └── Header
                └── UserAvatar

现在 App 中保存了用户信息,而 UserAvatar 需要读取它。

使用 Props 就需要:

App → Page → Layout → Header → UserAvatar

每个中间组件都要传一遍 user。

这种情况叫做 Props Drilling(属性逐层透传)。

React 提供了 Context 来解决这类问题。

8.1 Context 是什么?

一句话理解:

Context 允许组件共享数据,让深层子组件不必经过每一层 Props 传递,就能读取上层提供的值。

例如:

             App
              │
              │ 提供 UserContext
              ▼
            Page
              │
            Layout
              │
            Header
              │
              ▼
          UserAvatar
          直接读取 Context

8.2 最简单的 Context 示例

import { createContext, useContext } from "react";

// 1. 创建 Context
const UserContext = createContext("游客");

// 2. 深层子组件
function UserAvatar() {
  const username = useContext(UserContext);

  return <div>当前用户:{username}</div>;
}

// 3. 中间组件
function Header() {
  return <UserAvatar />;
}

// 4. 父组件
function App() {
  return (
    <UserContext.Provider value="张三">
      <Header />
    </UserContext.Provider>
  );
}

最终显示:

当前用户:张三

注意:

const username = useContext(UserContext);

即使没有通过 Props 一层层传递,UserAvatar 仍然能读取到上层 Provider 提供的值。

这里使用兼容 React 18 的 Provider 写法,React 19 也支持直接使用 <UserContext value="张三">。

8.3 Context 能结合 useState 使用吗?

当然可以,而且非常常见。

例如实现全局主题切换:

import {
  createContext,
  useContext,
  useState,
  type ReactNode
} from "react";

interface ThemeContextType {
  theme: "light" | "dark";
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | null>(null);

function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");

  const toggleTheme = () => {
    setTheme(prev => prev === "light" ? "dark" : "light");
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function ThemeButton() {
  const context = useContext(ThemeContext);

  if (!context) {
    throw new Error("ThemeButton 必须在 ThemeProvider 内使用");
  }

  return (
    <button onClick={context.toggleTheme}>
      当前主题:{context.theme}
    </button>
  );
}

function App() {
  return (
    <ThemeProvider>
      <ThemeButton />
    </ThemeProvider>
  );
}

交互演示

主题设置

image.png

Context 的主要优势:

  • 减少多层 Props 透传。
  • 适合主题、当前用户、语言等跨层共享数据。
  • 配合 useState 或 useReducer 可以共享状态及更新方法。

但要注意:Context 不代表所有状态都应该放在全局。 如果数据只被父子两个组件使用,直接用 Props 往往更简单。

九、进阶:受控组件与双向交互

现在来看一个非常常见的组件设计模式。

假设你封装一个搜索框,希望:

  1. 父组件控制搜索框中的文本。
  2. 子组件展示该文本。
  3. 用户输入时通知父组件。
  4. 父组件更新状态,再传回子组件。

这叫做受控组件模式。

import { useState } from "react";

interface SearchInputProps {
  value: string;
  onChange: (value: string) => void;
}

// 子组件
function SearchInput({ value, onChange }: SearchInputProps) {
  return (
    <input
      value={value}
      onChange={e => onChange(e.target.value)}
    />
  );
}

// 父组件
function App() {
  const [keyword, setKeyword] = useState("");

  return (
    <div>
      <SearchInput
        value={keyword}
        onChange={setKeyword}
      />

      <p>搜索关键词:{keyword}</p>
    </div>
  );
}

执行流程:

           父组件 App
                │
         value={keyword}
                │
                ▼
           SearchInput
                │
           用户输入内容
                │
       onChange(newValue)
                │
                ▼
          setKeyword()
                │
         父组件重新渲染
                │
                ▼
       新 value 传回子组件

看起来像“双向绑定”,但 React 的数据流本质仍然是单向的:

  • 数据通过 Props 向下传递。
  • 事件通过回调向上传递。

这种 value + onChange 模式在 React 组件库中非常普遍。

十、组件通信常见错误

10.1 子组件直接修改 Props

错误:

function Child({ user }: { user: User }) {
  user.name = "李四"; // ❌ 不应该修改 Props 对象
  return <div>{user.name}</div>;
}

正确方式是让父组件更新状态,再将新数据传给子组件。

interface ChildProps {
  user: User;
  onChangeName: (name: string) => void;
}

function Child({ user, onChangeName }: ChildProps) {
  return (
    <button onClick={() => onChangeName("李四")}>
      修改 {user.name}
    </button>
  );
}

10.2 错把函数调用结果传给 onClick

错误:

<button onClick={handleDelete(1)}>
  删除
</button>

这样会在渲染时执行函数,而不是点击时执行(假设 handleDelete 不是返回事件处理函数的高阶函数)。

正确:

<button onClick={() => handleDelete(1)}>
  删除
</button>

10.3 混淆 children 和 renderItem

// 普通插槽:直接传 JSX
<Card>
  <UserInfo />
</Card>

// Render Props:传函数
<MyList
  data={users}
  renderItem={(user) => <UserCard user={user} />}
/>

它们的核心区别就是:children 通常传现成的内容,renderItem 传生成内容的函数。

10.4 在多个兄弟组件中重复维护同一份状态

如果两个组件需要始终展示同一个状态,通常不要让它们各自维护独立副本。

更推荐把状态提升到共同父组件,确保有一个明确的数据来源。

十一、组件通信方式如何选择?

这是实际开发时最需要掌握的一张表。

场景推荐方案
父组件向子组件传数据Props
子组件通知父组件回调函数
兄弟组件共享状态状态提升
跨多层组件共享数据Context
组件传入自定义内容`children`
一个组件有多个内容区域具名 ReactNode Props
子组件提供数据,外部决定如何渲染Render Props
复杂跨页面全局状态Context 或专门的状态管理库

如果项目更加复杂,还可能接触:

  • Zustand:轻量全局状态管理。
  • Redux Toolkit:适合需要集中管理复杂状态的项目。
  • TanStack Query:侧重服务端数据获取、缓存与同步。
  • 自定义 Hook:复用状态逻辑,而不一定共享同一份状态。

它们并不是 Props 和 Context 的替代品,而是针对不同问题的工具。

十二、综合实战:设计一个可复用的任务卡片

最后将 Props、回调函数、children 和 TypeScript 组合起来。

目标:封装一个任务卡片,支持自定义内容和删除操作。

import type { ReactNode } from "react";

interface Task {
  id: number;
  name: string;
  status: string;
}

interface TaskCardProps {
  task: Task;
  children?: ReactNode;
  footer?: ReactNode;
  onDelete: (id: number) => void;
}

function TaskCard({
  task,
  children,
  footer,
  onDelete
}: TaskCardProps) {
  return (
    <div className="task-card">
      <h3>{task.name}</h3>

      <p>状态:{task.status}</p>

      <div className="task-content">
        {children}
      </div>

      <div className="task-footer">
        {footer}

        <button onClick={() => onDelete(task.id)}>
          删除
        </button>
      </div>
    </div>
  );
}

父组件使用:

import { useState } from "react";

function App() {
  const [tasks, setTasks] = useState<Task[]>([
    { id: 1, name: "视频回查", status: "running" },
    { id: 2, name: "文章回扫", status: "pending" }
  ]);

  const handleDelete = (id: number) => {
    setTasks(prev => prev.filter(task => task.id !== id));
  };

  return (
    <div>
      {tasks.map(task => (
        <TaskCard
          key={task.id}
          task={task}
          onDelete={handleDelete}
          footer={<button>查看详情</button>}
        >
          <p>任务 ID:{task.id}</p>
        </TaskCard>
      ))}
    </div>
  );
}

在这个例子中:

  • task={task}:父传子。
  • onDelete={handleDelete}:子组件通过回调通知父组件。
  • children:内容插槽。
  • footer:具名插槽。
  • useState:父组件管理数据。
  • map:动态生成任务卡片。

一个可复用的 React 组件,基本就是这样设计出来的。

十三、总结:掌握这五个概念就够了

image.png

React 最核心的设计思想有两个:

第一,数据单向流动。 父组件通过 Props 向子组件传递数据,子组件通过回调函数通知父组件。

第二,组合优于继承。 通过 children、具名插槽和 Render Props,把组件组合成更复杂的页面,而不是依靠复杂的继承关系。

建议学习顺序:Props → 回调函数 → 状态提升 → children → Render Props → Context。

参考官方文档:React Passing Props、React Sharing State、React useContext。