前言:什么是优雅编程
"优雅"并不是魔法,它意味着清晰、可维护和具有可预测行为的代码。在 React 应用中,优雅编程不仅会让项目团队更容易协作,还能降低调试和扩展的成本。本文将从状态管理、组件设计和 Hook 使用等角度,结合示例代码,探讨如何写出简洁、易读且高效的 React 代码。
全局状态管理:避免过度复杂,选择合适工具
不要因为"听说需要全局状态管理"就引入 Redux。在很多应用中,本地状态(useState
或 useReducer
)就足够了。只有当数据需要跨越多个组件共享且难以通过 props 传递时,才有必要使用全局状态。
在这种情况下,推荐使用 MobX 或 Zustand 等简单的状态管理库。业内文章指出,Redux 提供严格的状态控制,但配置和样板代码繁琐,使得项目变得腱胀闷雀,特别是小型和中级规模项目。相比之下,MobX 通过可视察对象自动追踪依赖,允许直接修改状态,几乎没有样板代码。如果你需要更轻量的方案,Zustand 也是一个不错的选择,其 API 基于 Hooks,易上手且不限制设计。
建议 :如果只是处理本地或远程数据请求,可优先考虑 React 自带的
useState
、useReducer
和useContext
,配合 TanStack Query 等数据获取库。如果确实需要全局状态,再选择 MobX 或 Zustand 等更简洁的方案,而不是过度复杂的 Redux。
React Hooks 与类组件:拥抱函数式
React 团队已明确推荐使用函数组件并通过 Hooks 管理状态和生命周期,它们更简洁、易于组合和测试。官方文档指出,虽然类组件仍受支持,但不再建议在新的代码中使用类组件。
下面是同一组件的类组件与函数组件对比:
jsx
// 类组件
class Counter extends React.Component {
state = { count: 0 };
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<button onClick={this.handleClick}>点击次数:{this.state.count}</button>
);
}
}
// 函数组件 + Hook
function Counter() {
const [count, setCount] = React.useState(0);
return (
<button onClick={() => setCount(count + 1)}>
点击次数:{count}
</button>
);
}
函数组件省去了 this
的复杂性,更易于阅读和组合。配合自定义 Hook,可以将状态逻辑抽离出来复用。例如:
jsx
// 自定义 Hook 提取计数逻辑
function useCounter(initialValue = 0) {
const [count, setCount] = React.useState(initialValue);
const increment = () => setCount((c) => c + 1);
return { count, increment };
}
function Counter() {
const { count, increment } = useCounter();
return <button onClick={increment}>点击次数:{count}</button>;
}
通过这种方式,我们不仅减少了重复代码,还能让组件更聚焦于 UI,逻辑更容易测试和维护。
多数开发者误以为为了性能需要大量使用 memoization,然而有文章指出,应用中 90% 的 useMemo
和 useCallback
都是多余的,去掉它们不仅不会降低性能,反而可能让初次渲染更快。
- 什么时候使用
useMemo
? 当某个计算非常耗费性能,并且其依赖数据很少变化时,使用useMemo
缓存结果可以避免重复计算。但如果计算开销很小,或依赖频繁变化,使用useMemo
反而会增加复杂度。 - 什么时候使用
useCallback
? 当函数作为 prop 传递给经常 re-render 的子组件,并且该函数没有随着每次渲染而变化,使用useCallback
可以避免子组件的无意义 re-render。但是 React 官方文档也提到编译器会自动帮你 memoize 一些简单的事件处理函数,在很多情况下不必手动使用。
示例:假设有一个组件渲染大量的列表项,每个列表项有一个按钮调用 onSelect
,且 onSelect
会传入当前项的 id。如果不使用 useCallback
,每次父组件渲染都会创建新的函数,导致 React.memo
的子组件全部重新渲染。
jsx
const Item = React.memo(function Item({ id, onSelect }) {
console.log('渲染 Item', id);
return <button onClick={() => onSelect(id)}>选择 {id}</button>;
});
function List({ items }) {
const [selected, setSelected] = React.useState(null);
// 如果不使用 useCallback,这里会在每次渲染生成新函数
const handleSelect = React.useCallback((id) => {
setSelected(id);
}, []);
return (
<div>
{items.map((id) => (
<Item key={id} id={id} onSelect={handleSelect} />
))}
<p>当前选择:{selected}</p>
</div>
);
}
// 曝露 focus 方法 React.useImperativeHandle(ref, () => ({ focus: () => { inputRef.current?.focus(); }, }));
return <input ref={inputRef} {...props} />; });
function Parent() { const fancyRef = React.useRef(); return ( <> <button onClick={() => fancyRef.current?.focus()}>点我聚焦 FancyInput </> ); }
markdown
这种模式让父组件只能调用 `focus` 方法,避免对内部 DOM 的直接操作,符合"最小可访问性"的原则。同时请注意,只有当你确实需要对 DOM 进行 imperative 操作或为第三方库提供接口时才使用 `forwardRef`。
## React.memo:按需优化
`React.memo` 是一个高阶组件,用于记忆函数组件的返回结果。如果传入相同的 props,它会跳过重新渲染。这在渲染成本昂超、且 props 很少变化时非常有用。例如列表项、复杂表单或数据可视化组件。
然而,滥用 `React.memo` 也会引入不必要的复杂性。权威文章指出,在以下场景使用 memo 化能带来明显好复。
- 父组件频繁渲染,但子组件的 props 在多数情况下保持不变。
- 渲染逻辑复杂,如大型列表、虚拟滚动表格、图表等。
而在以下情况应避免使用:
- 父组件每次都传入新的对象或函数(即使内容相同),导致 `React.memo` 失效。
- 子组件自身渲染简单,没有明显的性能瓶颈。
- 在组件树深度较浅或整体渲染开销本身不大时。
通过结合 `useMemo`、`useCallback` 和 `React.memo`,可以让真正昂超的子组件保持稳定,但切记不要将 `React.memo` 当作无差别的性能优化工具,而应作为精确的手术刀。
## useReactive(ahooks):优雅处理复杂对象状态
当状态是对象或数组时,使用 `useState` 更新嵌套字段会比较缓微:
```jsx
const [user, setUser] = React.useState({ name: '', address: { city: '' } });
// 更新城市时需要复制原始对象
const updateCity = (city) => {
setUser((prev) => ({
...prev,
address: { ...prev.address, city },
}));
};
这种更新方式易出错且代码繁琐。有些开发者用 immer 或自写 useImmer
来简化,但仍然需要通过 produce
返回新对象。
ahooks
提供的 useReactive
基于 Proxy
实现响应式对象,你可以直接修改属性而不需要调用 setter,简洁而优雅。例如:
jsx
import { useReactive } from 'ahooks';
function Profile() {
// 创建响应式对象
const state = useReactive({ count: 0, user: { name: '张三', age: 18 } });
return (
<div>
<p>姓名:{state.user.name}</p>
<p>年龄:{state.user.age}</p>
<p>计数:{state.count}</p>
<button onClick={() => (state.count += 1)}>增加计数</button>
<button onClick={() => (state.user.age += 1)}>年龄 +1</button>
</div>
);
}