组件与 Props
React 的核心是组件。每个组件是一个返回 JSX 的 JavaScript 函数,通过 props 接收外部输入。
组件的本质
// 组件 = 函数签名 props → JSX
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
// 等价于
const Greeting = ({ name }) => <h1>Hello, {name}!</h1>;React 会在每次渲染时调用组件函数,并把新的 props 传入。组件函数必须是纯函数(给定相同 props,渲染相同 UI,无副作用)。
Props 的核心特性
1. 只读性(不可变)
function Bad({ user }) {
user.name = 'Hacker'; // 🚨 不要修改 props
return <div>{user.name}</div>;
}
function Good({ user }) {
const [name, setName] = useState(user.name); // ✅ 复制到 state
return <input value={name} onChange={e => setName(e.target.value)} />;
}2. 任意 JS 值
Props 可以传递任何 JavaScript 值:
<Tooltip
content="保存" // 基本类型
position={{ x: 10, y: 5 }} // 对象
onConfirm={() => save()} // 函数
icon={<SaveIcon />} // JSX / React Element
roles={['admin', 'editor']} // 数组
/>🚨 陷阱:以内联对象或内联函数作为 prop,每次渲染都会创建新引用,可能破坏子组件的
memo优化。对于性能敏感的组件,使用useMemo/useCallback包裹。
3. children — 特殊的 Prop
// children 是组件标签之间的所有内容
function Card({ children, title }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// 用法
<Card title="用户信息">
<p>姓名:张三</p>
<p>邮箱:zhang@example.com</p>
</Card>💡 最佳实践:
children支持**组合(Composition)**优于继承。几乎所有 UI 库都使用 children 构建灵活的布局组件。
组件组合模式
1. 容器/展示分离
// 容器组件:负责数据和逻辑
function UserListContainer() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetchUsers().then(setUsers);
}, []);
return <UserList users={users} />;
}
// 展示组件:只负责渲染
function UserList({ users }) {
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}2. 插槽(Slots)模式
function Layout({ header, sidebar, children }) {
return (
<div className="layout">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
// 使用
<Layout header={<Navbar />} sidebar={<Sidebar />}>
<Dashboard />
</Layout>3. Render Props
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url).then(r => r.json()).then(setData);
}, [url]);
if (!data) return <div>Loading...</div>;
return <>{render(data)}</>;
}
// 使用
<DataFetcher
url="/api/users"
render={users => <UserTable users={users} />}
/>💡 最佳实践:Render Props 已被 Hooks 和自定义组件大幅取代。在需要复用有状态行为 + 自定义渲染时可以考虑,但优先使用自定义 Hook。
4. 高阶组件(HOC)
// 注入 loading 状态
function withLoading(WrappedComponent) {
return function WithLoading({ isLoading, ...props }) {
if (isLoading) return <Spinner />;
return <WrappedComponent {...props} />;
};
}
const UserProfileWithLoading = withLoading(UserProfile);💡 最佳实践:HOC 是 React 旧时代的复用模式。现代 React 优先使用自定义 Hooks 解决同样的需求。
组件生命周期(函数组件视角)
React 函数组件没有"生命周期方法",但可以通过 Hooks 覆盖所有生命周期场景:
| Class 生命周期 | 函数组件等价 |
|---|---|
componentDidMount |
useEffect(() => {}, []) |
componentDidUpdate |
useEffect(() => {}) 或 useEffect(() => {}, [deps]) |
componentWillUnmount |
useEffect 返回的清理函数 |
shouldComponentUpdate |
React.memo |
componentDidCatch |
仍用 Error Boundary(class 组件) |
function UserProfile({ userId }) {
useEffect(() => {
// componentDidMount + componentDidUpdate(依赖变化)
fetchUser(userId);
// componentWillUnmount
return () => cancelPendingRequest();
}, [userId]);
}组件拆分原则
| 信号 | 拆分规则 |
|---|---|
| 单文件 > 150 行 | 考虑拆分 |
| 一个组件做很多事 | 提取子组件 |
| 多组件共享 UI 逻辑 | 提取自定义 Hook |
| 多处复用同一 UI 片段 | 提取为通用组件 |
| 渲染与数据逻辑耦合 | 分离为容器 + 展示 |
💡 最佳实践:拆分组件时问自己三个问题 — “这个组件只做一件事吗?可以不用注释就理解吗?能在不改代码的情况下复用它吗?”
defaultProps vs 默认参数
// React 18+ 推荐:JavaScript 默认参数
function Button({ text = '确认', onClick, size = 'md' }) {
return <button onClick={onClick} className={`btn-${size}`}>{text}</button>;
}
// 老式写法(不推荐)
Button.defaultProps = {
text: '确认',
size: 'md',
};💡 最佳实践:函数组件用 JavaScript 默认参数。
defaultProps是 class 组件遗留,在函数组件上已标记为 deprecated。
受控 vs 非受控
这是 React 组件设计中的关键抉择:
// 受控组件:状态由父组件管理
<Modal open={isOpen} onClose={() => setIsOpen(false)} />
// 非受控组件:自己管理状态
<Modal defaultOpen={true} />大多数情况下,受控组件更灵活、可预测。非受控组件适合快速开发或与第三方库集成。
🚨 常见陷阱
- 修改 props:props 是只读的,需要修改时用 state
- 忘记 key:列表渲染必须有稳定的 key,且 key 要在 map 的最外层
- 过度拆分:不要为了拆而拆;保持同一"责任"的代码在一起
- 内联对象/函数:频繁渲染的组件中作为 prop 传递内联对象会破坏 memo
- 组件命名不够语义化:
Item太泛,UserListItem更明确