组件化思维:从拆页面到拆状态
很多项目做久了会变成「一个巨型组件加十几个布尔开关」,根因不是没拆组件,而是按视觉区域拆而不是按状态边界拆。正确的切分依据是:这块状态由谁拥有、谁需要读、谁需要改。状态放在拥有它的最近公共祖先;只读的通过属性传递,需要改的传函数或回调。
组件化的本质是数据流设计:把状态收敛到少数几个位置,其余部分只负责渲染,界面不一致的问题才会从根上消失。
- 状态边界同一份状态只保留一个来源,其余层级派生计算。
- 可推导就不存能由已有状态算出的值,不要另存一份。
- 纯展示组件只接收属性并渲染,便于复用与测试。
- 受控与派生表单元素受控于状态,列表项由数据映射生成。
函数组件与 Hooks 规则
React 依靠调用顺序来识别每一个 Hook,因此 Hooks 只能在组件或自定义 Hook 的顶层调用,不能写进条件分支与循环。这条规则带来的连锁约束是:需要条件逻辑时,把条件放在 Hook 内部,或者把分支整体提取成不同组件,而不是在分支里调用 Hook。
副作用统一收敛在 useEffect 中,但要清楚它不是生命周期钩子,而是「同步外部系统」的机制。判断是否该用副作用,标准是:这段逻辑是否与 React 之外的系统交互(网络、订阅、DOM 直写、定时器)。仅仅是「某值变了要算另一个值」,应该用 useMemo 或直接计算。
function useDebounced(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer); // 清理:值变化时取消上一次
}, [value, delay]);
return debounced;
}
这个自定义 Hook 展示了副作用的完整形态:注册副作用 + 返回清理函数。清理函数不做,输入框快速输入时会留下大量未完成的定时器,最终把过期结果写进状态,表现出明显的「输入卡顿」。
状态放置与数据流设计
状态放置的第一原则是「尽可能低」。把筛选条件放在列表组件内部,把弹窗开关放在触发它的父组件上,能显著减少无关重渲染。当状态需要跨多层传递时,优先考虑组件组合(把子元素作为属性传入)而不是层层透传;确实需要跨模块共享时,再引入 Context 或外部状态库。
- 局部界面态展开、悬停、输入内容,放在组件内部即可。
- 服务端数据交给带有缓存与失效策略的数据请求库,不要手写全局变量。
- 跨页面共享登录信息、主题等低频变化值用 Context,并拆分多个 Context 避免连带更新。
性能优化:memo、useMemo 与副作用边界
性能优化的顺序永远是:先测量,再定位,最后才改动。React 开发者工具的性能面板能记录一次提交里哪些组件重渲染、耗时多少。真实的瓶颈通常集中在三处:长列表未虚拟化、每次渲染都创建新对象导致的属性不稳定、以及高开销计算写在了渲染路径上。
const rows = useMemo(
() => items.filter((it) => it.name.includes(keyword)),
[items, keyword]
);
const onSelect = useCallback((id) => dispatch(select(id)), [dispatch]);
return <Table rows={rows} onSelect={onSelect} />;
useMemo 缓存计算结果,useCallback 稳定函数引用,二者配合 React.memo 才能让子组件真正跳过重渲染。凡尔登式的「到处加 memo」往往收益甚微,因为属性每天都在变,缓存本身还有成本。
并发特性与 Suspense
React 18 引入的并发渲染允许把一次更新标记为非紧急,让输入响应优先完成。useTransition 把「切换列表筛选」这类耗时更新放到后台,输入框保持即时反馈;useDeferredValue 则直接延时某个值,适合无法改造更新逻辑的场景。Suspense 配合懒加载分包,是首屏体积优化的标准手段。
需要注意的是并发特性不是免费的:它改变了副作用的执行时机,某些在渲染中依赖顺序的代码可能表现异常。项目刚引入时应先在低风险模块试用,配合严格的副作用清理习惯一起推进。
工程化与常见坑
列表渲染缺少稳定且唯一的 key 是最常见的坑。用数组下标作 key,在中间插入或删除元素时,React 会把旧组件的状态错配到新位置,表现为输入框内容串行、动画错乱、组件内部状态莫名重置。key 应当使用业务主键,且在同级列表内保证唯一稳定。
受控与非受控混用也会出问题:受控输入如果 value 没有同步更新,输入框会「锁死」无法输入。另外,把对象或数组直接写进依赖数组,每次渲染都是新引用,副作用会无限触发——正确做法是把依赖拆成基础类型,或在副作用内部做必要的比较。
// ❌ 依赖里的对象每次都是新引用,副作用会不停地执行
useEffect(() => { sync(options); }, [options]);
// ✅ 依赖收敛到基础类型,或在外层稳定 options 的引用
useEffect(() => { sync({ page, size }); }, [page, size]);
工程配置上,建议开启严格模式以便尽早暴露不纯的渲染逻辑;加上类型检查,让属性契约在编译期就被验证;用错误边界包住路由级组件,避免某个子组件抛错导致整页空白。
小结与延伸阅读
把状态放在正确的位置,比掌握任何优化技巧都重要;优化只是让正确的结构跑得更快。
- 异步基础不牢,回看《JavaScript 核心进阶:异步与模块化》。
- 偏好另一种组件模型,可对读《Vue 3 组合式 API 开发指南》比较两者差异。
- 要串联服务端,读《全栈开发:Node.js + 数据库实战》。