本文讲解 React 中最常用的组件通信方式,以及如何用「组合」思想封装可复用组件。
每个知识点都配有完整代码和逐行说明,零基础也能跟着学。

适用版本:React 18+(代码在 React 17 同样可用)

一、为什么需要组件通信?

React 应用由一棵「组件树」组成:

    App(根组件)
     ├── Header
     ├── Sidebar
     └── MainContent
           ├── SearchBar
           └── UserList

数据通常「从上往下流」:父组件知道全局状态,子组件只负责展示和交互。
当子组件发生用户操作(点击、输入)时,又需要把结果「从下往上」告诉父组件。

掌握下面四种模式,就能应对 90% 的日常开发场景:

  1. props        —— 父传子(数据 + 配置)
  2. callback     —— 子传父(事件 + 结果)
  3. children / Render Props —— 组合式复用
  4. 受控 / 非受控 —— 封装输入类组件


二、props:父组件向子组件传数据

【核心概念】
props 是只读的。子组件不能修改 props,只能读取并使用。

【示例:父组件把用户名传给子组件展示】

// ---------- 子组件 Greeting.jsx ----------
function Greeting(props) {
  // props 是一个对象,包含父组件传入的所有属性
  return (
    <div>
      <h1>你好,{props.name}!</h1>
      <p>今天是 {props.date}</p>
    </div>
  );
}

// 也可以用解构写法,更简洁:
function Greeting({ name, date }) {
  return (
    <div>
      <h1>你好,{name}!</h1>
      <p>今天是 {date}</p>
    </div>
  );
}


// ---------- 父组件 App.jsx ----------
function App() {
  const userName = "小明";
  const today = "2026年6月20日";

  return (
    <div>
      {/* 通过属性名 = 值 的方式传递 props */}
      <Greeting name={userName} date={today} />
    </div>
  );
}

【要点总结】
  - 字符串可以直接写:  <Button label="提交" />
  - 变量/表达式用花括号:<Button label={buttonText} />
  - 传递函数也是 props:<Button onClick={handleClick} />
  - 可以传任意类型:数字、数组、对象、JSX、甚至另一个组件


三、callback:子组件向父组件传数据

【核心概念】
React 没有内置的「子传父」API。做法是:
  父组件定义一个函数 → 通过 props 传给子 → 子在合适时机调用这个函数

这叫做「回调函数(callback)」模式,本质是:父把「电话」交给子,子有事就「打回来」。

【示例:计数器 —— 子组件按钮,父组件管数字】

// ---------- 子组件 CounterButton.jsx ----------
function CounterButton({ count, onIncrement, onDecrement }) {
  return (
    <div>
      <p>当前计数:{count}</p>
      {/* 点击时调用父组件传下来的函数 */}
      <button onClick={onIncrement}>+1</button>
      <button onClick={onDecrement}>-1</button>
    </div>
  );
}


// ---------- 父组件 App.jsx ----------
import { useState } from "react";

function App() {
  // 状态放在父组件 —— 这是 React 的推荐做法(状态提升)
  const [count, setCount] = useState(0);

  // 定义回调函数
  const handleIncrement = () => {
    setCount(count + 1);
  };

  const handleDecrement = () => {
    setCount(count - 1);
  };

  return (
    <CounterButton
      count={count}                    // 父传子:当前值
      onIncrement={handleIncrement}    // 父传子:加一回调
      onDecrement={handleDecrement}    // 父传子:减一回调
    />
  );
}

【数据流向图】

  父组件 App
    │  count={0}          ──props──►  子组件 CounterButton 显示 0
    │  onIncrement={fn}   ──props──►  子组件保存 fn
    │
    │  用户点击 +1
    │  ◄──callback──  子组件调用 onIncrement()
    │  setCount(1)      父组件更新 state
    │  count={1}        ──props──►  子组件重新渲染,显示 1

【命名约定】
  - 传给子的事件处理函数,通常以 on 开头:onClick、onChange、onSubmit
  - 子组件内部的处理器,通常以 handle 开头:handleClick、handleChange

【带参数的回调】

// 子组件:搜索框
function SearchInput({ onSearch }) {
  const [keyword, setKeyword] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    // 把搜索关键词「回传」给父组件
    onSearch(keyword);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
      />
      <button type="submit">搜索</button>
    </form>
  );
}

// 父组件
function App() {
  const handleSearch = (keyword) => {
    console.log("用户搜索了:", keyword);
    // 这里可以发请求、过滤列表等
  };

  return <SearchInput onSearch={handleSearch} />;
}


四、组合设计:children 与 Render Props

当多个组件有相同「外壳」但「内容不同」时,不要复制粘贴,用「组合」复用结构。

4.1 children —— 最简单的组合

【思路】父组件把 JSX 当作 props.children 传给子组件,子决定在哪里渲染。

// ---------- 通用卡片外壳 ----------
function Card({ title, children }) {
  return (
    <div className="card">
      <div className="card-header">{title}</div>
      <div className="card-body">
        {children}   {/* 这里渲染父组件「塞进来」的内容 */}
      </div>
    </div>
  );
}

// ---------- 使用方式 ----------
function App() {
  return (
    <div>
      {/* 卡片 A:放用户信息 */}
      <Card title="个人资料">
        <p>姓名:小明</p>
        <p>年龄:25</p>
      </Card>

      {/* 卡片 B:放表单 —— 同一个 Card,不同内容 */}
      <Card title="修改密码">
        <input type="password" placeholder="新密码" />
        <button>保存</button>
      </Card>
    </div>
  );
}

Card 组件不需要知道里面是什么,只负责布局和标题。这就是「组合优于继承」。


4.2 Render Props —— 把渲染逻辑也交给调用方

【思路】子组件负责「逻辑和数据」,通过函数 prop 把数据交给父组件决定「怎么画」。

// ---------- 数据获取组件(只管逻辑) ----------
function DataFetcher({ url, render }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((json) => {
        setData(json);
        setLoading(false);
      })
      .catch((err) => {
        setError(err);
        setLoading(false);
      });
  }, [url]);

  // 把状态交给 render 函数,由调用方决定 UI
  return render({ data, loading, error });
}

// ---------- 使用方式:同一套逻辑,不同 UI ----------
function UserPage() {
  return (
    <DataFetcher
      url="/api/users"
      render={({ data, loading, error }) => {
        if (loading) return <p>加载中...</p>;
        if (error)   return <p>出错了:{error.message}</p>;
        return (
          <ul>
            {data.map((user) => (
              <li key={user.id}>{user.name}</li>
            ))}
          </ul>
        );
      }}
    />
  );
}

function UserTablePage() {
  return (
    <DataFetcher
      url="/api/users"
      render={({ data, loading, error }) => {
        if (loading) return <p>加载中...</p>;
        if (error)   return <p>出错了</p>;
        // 同样的数据,渲染成表格
        return (
          <table>
            <tbody>
              {data.map((user) => (
                <tr key={user.id}>
                  <td>{user.name}</td>
                  <td>{user.email}</td>
                </tr>
              ))}
            </tbody>
          </table>
        );
      }}
    />
  );
}

【children vs Render Props 怎么选?】
  - children:结构固定,只是「插槽」不同 → 用 children
  - 需要把内部 state/logic 暴露给外部 → 用 Render Props(或自定义 Hook)


4.3 综合示例:Modal 弹窗组件

function Modal({ isOpen, onClose, title, children }) {
  if (!isOpen) return null;

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h2>{title}</h2>
          <button onClick={onClose}>×</button>
        </div>
        <div className="modal-body">{children}</div>
      </div>
    </div>
  );
}

// 使用
function App() {
  const [showModal, setShowModal] = useState(false);

  return (
    <>
      <button onClick={() => setShowModal(true)}>打开弹窗</button>

      <Modal
        isOpen={showModal}
        onClose={() => setShowModal(false)}   // callback:子通知父关闭
        title="确认删除"
      >
        <p>确定要删除这条记录吗?</p>
        <button onClick={() => setShowModal(false)}>取消</button>
        <button onClick={handleDelete}>确定</button>
      </Modal>
    </>
  );
}

这个例子同时用到了:props(isOpen、title)、callback(onClose)、children(弹窗内容)。


五、受控 vs 非受控:封装可复用的输入组件

封装 Input、Select、Switch 等表单组件时,必须决定:状态由谁管?

5.1 受控组件(Controlled)—— 推荐默认方案

【定义】组件的值完全由父组件的 state 控制,通过 value + onChange 双向绑定。

// ---------- 受控 Input 封装 ----------
function TextInput({ label, value, onChange, placeholder }) {
  return (
    <div className="form-item">
      <label>{label}</label>
      <input
        value={value}                              // 值来自父组件
        onChange={(e) => onChange(e.target.value)} // 变化通知父组件
        placeholder={placeholder}
      />
    </div>
  );
}

// ---------- 父组件使用 ----------
function LoginForm() {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({ username, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <TextInput
        label="用户名"
        value={username}
        onChange={setUsername}
        placeholder="请输入用户名"
      />
      <TextInput
        label="密码"
        value={password}
        onChange={setPassword}
        placeholder="请输入密码"
      />
      <button type="submit">登录</button>
    </form>
  );
}

【优点】
  - 父组件随时能读取、校验、重置值
  - 多个输入框联动方便(如:确认密码必须和密码一致)
  - 便于做表单级验证和提交


5.2 非受控组件(Uncontrolled)—— 简单场景或性能优化

【定义】组件内部自己管 state,父组件通过 ref 在需要时「拿」值。

import { useState, useRef } from "react";

// ---------- 非受控 Input 封装 ----------
function UncontrolledInput({ label, defaultValue, placeholder, inputRef }) {
  return (
    <div className="form-item">
      <label>{label}</label>
      <input
        ref={inputRef}              // 父组件通过 ref 访问 DOM
        defaultValue={defaultValue} // 只设置初始值,之后组件自己管
        placeholder={placeholder}
      />
    </div>
  );
}

// ---------- 父组件使用 ----------
function QuickSearch() {
  const inputRef = useRef(null);

  const handleSearch = () => {
    // 只在点击时读取一次值
    const keyword = inputRef.current.value;
    console.log("搜索:", keyword);
  };

  return (
    <div>
      <UncontrolledInput
        label="关键词"
        placeholder="输入后点击搜索"
        inputRef={inputRef}
      />
      <button onClick={handleSearch}>搜索</button>
    </div>
  );
}

【适用场景】
  - 简单表单,只在提交时读一次值
  - 文件上传(file input 通常是非受控的)
  - 与第三方非 React 库集成


5.3 最佳实践:同时支持受控和非受控

成熟的组件库(如 Ant Design)会同时支持两种模式。
判断规则:如果父组件传了 value,就是受控;否则走内部 state。

function FlexibleInput({ label, value, defaultValue, onChange, placeholder }) {
  // 判断是否受控:父组件是否传了 value
  const isControlled = value !== undefined;

  const [internalValue, setInternalValue] = useState(defaultValue ?? "");

  // 实际显示的值
  const displayValue = isControlled ? value : internalValue;

  const handleChange = (e) => {
    const newValue = e.target.value;

    if (!isControlled) {
      setInternalValue(newValue);  // 非受控:更新内部 state
    }
    // 无论哪种模式,都通知父组件(如果父组件关心的话)
    onChange?.(newValue);
  };

  return (
    <div className="form-item">
      <label>{label}</label>
      <input
        value={displayValue}
        onChange={handleChange}
        placeholder={placeholder}
      />
    </div>
  );
}

// 受控用法
<FlexibleInput value={name} onChange={setName} label="姓名" />

// 非受控用法(只设初始值,组件自己管)
<FlexibleInput defaultValue="默认值" label="备注" />


5.4 受控 Switch 开关示例

function Switch({ checked, onChange, label }) {
  return (
    <label className="switch">
      <input
        type="checkbox"
        checked={checked}
        onChange={(e) => onChange(e.target.checked)}
      />
      <span className="slider" />
      {label && <span>{label}</span>}
    </label>
  );
}

function SettingsPanel() {
  const [darkMode, setDarkMode] = useState(false);
  const [notifications, setNotifications] = useState(true);

  return (
    <div>
      <Switch
        label="深色模式"
        checked={darkMode}
        onChange={setDarkMode}
      />
      <Switch
        label="消息通知"
        checked={notifications}
        onChange={setNotifications}
      />
      <p>当前:{darkMode ? "深色" : "浅色"} / 通知{notifications ? "开" : "关"}</p>
    </div>
  );
}


六、完整小项目:Todo 列表(综合运用)

把前面所有知识点串起来:

import { useState } from "react";

// ---- 1. 可复用的受控输入框 ----
function Input({ value, onChange, placeholder }) {
  return (
    <input
      value={value}
      onChange={(e) => onChange(e.target.value)}
      placeholder={placeholder}
    />
  );
}

// ---- 2. Todo 单项(props + callback) ----
function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li style={{ textDecoration: todo.done ? "line-through" : "none" }}>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={() => onToggle(todo.id)}      // callback 回传 id
      />
      {todo.text}
      <button onClick={() => onDelete(todo.id)}>删除</button>
    </li>
  );
}

// ---- 3. Todo 列表容器(children 组合思想:接收 render 或 map) ----
function TodoList({ todos, onToggle, onDelete }) {
  if (todos.length === 0) {
    return <p>暂无待办,添加一条吧!</p>;
  }

  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </ul>
  );
}

// ---- 4. 根组件:状态提升,统一调度 ----
function TodoApp() {
  const [text, setText] = useState("");
  const [todos, setTodos] = useState([]);

  const addTodo = () => {
    if (!text.trim()) return;
    setTodos([...todos, { id: Date.now(), text, done: false }]);
    setText("");  // 受控输入框:添加后清空
  };

  const toggleTodo = (id) => {
    setTodos(todos.map((t) =>
      t.id === id ? { ...t, done: !t.done } : t
    ));
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter((t) => t.id !== id));
  };

  return (
    <div>
      <h1>我的待办</h1>
      <div>
        <Input
          value={text}
          onChange={setText}
          placeholder="输入待办事项"
        />
        <button onClick={addTodo}>添加</button>
      </div>
      <TodoList
        todos={todos}
        onToggle={toggleTodo}
        onDelete={deleteTodo}
      />
      <p>共 {todos.length} 项,已完成 {todos.filter(t => t.done).length} 项</p>
    </div>
  );
}

【这个例子体现了什么?】
  - Input:受控组件封装,value + onChange
  - TodoItem:props 接收数据,callback 上报操作
  - TodoList:组合多个 TodoItem,父级统一传回调
  - TodoApp:状态提升,所有数据在顶层,单向数据流清晰


七、设计原则速查表

┌──────────────────┬─────────────────────────────────────────────────────┐
│ 场景             │ 推荐做法                                            │
├──────────────────┼─────────────────────────────────────────────────────┤
│ 父传数据给子     │ props                                               │
│ 子通知父发生了啥 │ callback(通过 props 传函数)                       │
│ 兄弟组件通信     │ 状态提升到共同父组件,或通过 Context(进阶)        │
│ 相同外壳不同内容 │ children 组合                                       │
│ 共享逻辑不同 UI  │ Render Props 或自定义 Hook                          │
│ 表单输入封装     │ 默认受控;简单场景可用非受控;库组件两者都支持      │
│ 状态放哪         │ 谁需要读/写,状态就放在谁那里;多处用到就提升       │
└──────────────────┴─────────────────────────────────────────────────────┘


八、常见误区

误区 1:在子组件里直接修改 props
  ❌  props.name = "新名字";   // 报错!props 是只读的
  ✅  通过 callback 通知父组件,由父组件改 state 再传下来

误区 2:受控组件没有 onChange
  ❌  <input value={text} />   // 有 value 无 onChange,输入框会「冻住」
  ✅  <input value={text} onChange={e => setText(e.target.value)} />

误区 3:callback 里直接修改 state 对象
  ❌  todo.done = true; setTodos(todos);   // React 检测不到变化,不会重渲染
  ✅  setTodos(todos.map(t => t.id === id ? {...t, done: true} : t));

误区 4:过度使用 Context 代替 props
  只有「跨很多层」才需要 Context。父子/爷孙通信用 props + callback 更简单清晰。


 

更多推荐