编写健壮的组件
参考:Writing Resilient Components - Dan Abramov
核心原则
有四个重要的组件设计原则,每个组件都应该努力遵循:
- 不要阻止数据流(Don’t Stop the Data Flow)
- 始终准备好渲染(Always Be Ready to Render)
- 没有组件是单例(No Component is a Singleton)
- 保持本地状态隔离(Keep the Local State Isolated)
原则 1:不要阻止数据流
不要在渲染中阻止数据流
当有人使用你的组件时,他们期望可以随时间传递不同的 props,并且组件会反映这些变化:
// isOk 可能由状态驱动,可以随时改变
<Button color={isOk ? 'blue' : 'red'} />一般来说,这就是 React 的默认工作方式。如果你在 Button 组件内部使用 color prop,你会看到该次渲染提供的值:
function Button({ color, children }) {
return (
// `color` 总是最新的!
<button className={'Button-' + color}>
{children}
</button>
);
}常见错误:将 props 复制到 state
// 错误:将 props 复制到 state
class Button extends React.Component {
state = {
color: this.props.color
};
render() {
const { color } = this.state; // `color` 是过时的!
return <button className={'Button-' + color}>...</button>;
}
}问题:如果 color prop 改变,state 不会更新,因为 state 只在组件首次创建时初始化。
正确做法:直接使用 props
// 正确:直接使用 props
function Button({ color, children }) {
return (
<button className={'Button-' + color}>
{children}
</button>
);
}不要在 Effects 中阻止数据流
// 错误:在 effect 中复制 props 到 state
function Button({ color }) {
const [internalColor, setInternalColor] = useState(color);
useEffect(() => {
setInternalColor(color);
}, [color]);
return <button className={'Button-' + internalColor}>...</button>;
}问题:这增加了不必要的复杂性。直接使用 prop 更简单。
正确做法:直接使用 props
// 正确:直接使用 props
function Button({ color, children }) {
return (
<button className={'Button-' + color}>
{children}
</button>
);
}原则 2:始终准备好渲染
不要在渲染期间执行副作用
// 错误:在渲染期间执行副作用
function SearchResults({ query }) {
if (query === null) {
return null;
}
// 在渲染期间执行副作用
fetchData(query);
return <Results data={data} />;
}问题:每次渲染都会触发数据获取,可能导致无限循环。
正确做法:在 effect 中执行副作用
// 正确:在 effect 中执行副作用
function SearchResults({ query }) {
const [data, setData] = useState(null);
useEffect(() => {
if (query !== null) {
fetchData(query).then(setData);
}
}, [query]);
if (query === null) {
return null;
}
return <Results data={data} />;
}不要假设组件只渲染一次
// 错误:假设组件只渲染一次
function Component() {
useEffect(() => {
// 假设这只会运行一次
loadData();
}, []);
return <div>...</div>;
}问题:组件可能会因为各种原因重新渲染(props 变化、父组件重新渲染等)。
正确做法:准备好多次渲染
// 正确:准备好多次渲染
function Component({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
// 每次 id 变化时重新加载
loadData(id).then(setData);
}, [id]);
if (!data) {
return <div>Loading...</div>;
}
return <div>...</div>;
}原则 3:没有组件是单例
不要假设组件只存在一个实例
// 错误:假设组件是单例
let isMounted = false;
function Component() {
useEffect(() => {
if (!isMounted) {
isMounted = true;
initialize();
}
}, []);
return <div>...</div>;
}问题:如果页面中有多个 Component 实例,只有第一个会初始化。
正确做法:每个实例独立管理状态
// 正确:每个实例独立管理状态
function Component() {
useEffect(() => {
initialize();
return () => {
cleanup();
};
}, []);
return <div>...</div>;
}不要使用模块级变量存储组件状态
// 错误:使用模块级变量
let cache = {};
function Component({ id }) {
if (cache[id]) {
return <div>{cache[id]}</div>;
}
useEffect(() => {
fetchData(id).then(data => {
cache[id] = data;
});
}, [id]);
return <div>Loading...</div>;
}问题:所有组件实例共享同一个缓存,可能导致状态混乱。
正确做法:使用 state 或 Context
// 正确:使用 state
function Component({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchData(id).then(setData);
}, [id]);
if (!data) {
return <div>Loading...</div>;
}
return <div>{data}</div>;
}原则 4:保持本地状态隔离
不要将不相关的状态放在一起
// 错误:不相关的状态放在一起
function Component() {
const [state, setState] = useState({
count: 0,
name: 'Alice',
items: []
});
return (
<div>
<button onClick={() => setState({ ...state, count: state.count + 1 })}>
Count: {state.count}
</button>
<input
value={state.name}
onChange={e => setState({ ...state, name: e.target.value })}
/>
</div>
);
}问题:更新一个字段需要复制整个 state,容易出错。
正确做法:分离不相关的状态
// 正确:分离不相关的状态
function Component() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Alice');
const [items, setItems] = useState([]);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
);
}使用 useReducer 管理复杂状态
// 正确:使用 useReducer 管理复杂状态
function Component() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
name: 'Alice',
items: []
});
return (
<div>
<button onClick={() => dispatch({ type: 'increment' })}>
Count: {state.count}
</button>
<input
value={state.name}
onChange={e => dispatch({ type: 'setName', name: e.target.value })}
/>
</div>
);
}检查清单
在编写组件时,检查以下问题:
- 组件是否直接使用 props,而不是复制到 state?
- 组件是否准备好多次渲染?
- 组件是否不假设自己是单例?
- 组件是否将不相关的状态分离?
- 组件是否在 effect 中执行副作用,而不是在渲染期间?
- 组件是否使用 state 或 Context 管理状态,而不是模块级变量?
参考资源
关键要点:编写健壮的组件需要遵循数据流、准备好多次渲染、不假设单例、并保持状态隔离。这些原则适用于任何具有单向数据流的 UI 组件模型。