React 组件化通信与组合设计 —— 新手自学指南
本文讲解 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 更简单清晰。
更多推荐



所有评论(0)