React Hooks 四大主线:数据驱动、副作用、状态传递与派生

发布时间:2026/10/11 23:55:04
React Hooks 四大主线:数据驱动、副作用、状态传递与派生
做 React 开发这几年我最大的体会是Hooks 不是一套新的 API而是一套重新理解前端开发的思维方式。很多人学 Hooks 停在useState 存数据、useEffect 发请求的表面用法但真正让 Hooks 值回票价的是它把数据驱动这件事从口号变成了工程实践。数据驱动、副作用、状态传递、状态派生——这四个词看起来像概念堆砌其实是 React Hooks 设计背后的四条主线。我这次就把这四条线彻底捋一遍结合一个实际项目来讲透。文章适合三类人刚学完 React 基础但被 Hooks 各种规则绕晕的新手、从类组件迁移过来的老开发、以及想把手头代码写得更干净的中级工程师。前两类人可以重点看前四章第三类人建议直接看后面的实战案例和排查实录。我不讲那些文档里已经写烂了的入门示例直接从工程视角切入说说每个 API 背后到底在解决什么问题。1. 先说清楚Hooks 到底帮我们解决了什么1.1 类组件时代的三个痛点在 Hooks 出现之前React 函数组件只能做纯渲染凡是涉及状态和生命周期的地方都得写类组件。类组件本身不算难但实际项目一复杂问题就来了。第一是状态逻辑无法复用。比如两个组件都要做请求远程数据这件事类组件时期只能靠高阶组件或者 render props。高阶组件一层套一层调试的时候组件树里全是WithSomething这种东西代码跳来跳去看半天都搞不清楚数据从哪来、改到哪去。render props 也一样回调套回调可读性很差。第二是生命周期函数把不相关的代码硬塞在一起。componentDidMount里你可能同时在做数据请求、绑定事件、初始化定时器componentWillUnmount里再清理这一堆东西。这些操作之间没有任何逻辑关联只是因为它们碰巧在这个组件的生命周期的同一个时间点执行。代码量一上去这个类组件就成了一个收容所什么逻辑都往里面塞。第三是this绑定问题和状态更新的心智负担。写类组件要时刻记得 bind 事件处理函数忘了 bind 就报undefined is not a function。setState还是异步的想基于旧状态算新状态要写setState(prev ...)的函数式写法很多人一不小心就直接this.state.count之类的错误操作。更坑的是在异步回调里拿到的this.state可能已经过期了。这三个痛点的根源是同一个类组件的代码组织方式是按生命周期切分而不是按业务逻辑切分。你真正关心的是一段业务逻辑从开始到结束的完整链路但类组件强迫你把它拆成几段埋到不同的生命周期函数里过两周再回来维护只能靠脑内来回拼接。1.2 Hooks 的答案把组件当状态到 UI 的函数Hooks 换了一个视角组件本质上就是一个函数输入是 props 和 state输出是你想渲染的 UI。这个思路用一个式子表达就是UI f(state, props)只要状态不变UI 就不应该变状态变了React 负责重新调用这个函数渲染出新的 UI。开发者不再需要手动操作 DOM、不需要关心数据变了以后哪个节点要更新只需要回答问题给定当前状态我的界面应该长什么样这跟命令式编程的区别非常像。命令式编程就像你开车时每一步都在指挥踩离合、挂挡、给油、松离合声明式编程就像你告诉车我要去某地剩下的交给导航和变速箱。React 就是你的变速箱Hooks 就是让函数组件变聪明的动力系统。在 Hooks 体系里useState负责提供记忆能力让函数组件在多次渲染之间保存数据useEffect负责打破纯函数的限制让组件可以和外部世界打交道useContext负责在大范围内共享数据useMemo负责把计算结果缓存起来。这四件套正好对应标题里的四个关键词数据驱动、副作用、状态传递、状态派生。2. 数据驱动与副作用React 世界的两条主线2.1 数据驱动UI 是状态的投影而不是 DOM 的操作数据驱动这个词在工业领域和前端领域都出现了比如用传感器数据驱动轴承故障诊断、用振动信号驱动产线状态判断道理其实相通数据是源头决策和呈现是下游数据一变下游自动跟着变。React Hooks 里的数据驱动也是这套逻辑。页面上的一个列表、一个按钮的文案、一个图表的显示方式在数据驱动思维下都不应该被手动改变而应该从当前状态里推导出来。比如一个设备监控看板设备列表是数据选中了哪台设备是状态右侧详细面板的内容就是这两者的投影——数据变了投影就变了。实际操作中很多人写 React 代码还是命令式思维// 反模式手动改 DOM 或者用 ref 去操纵界面 const handleSubmit () { const el document.getElementById(list); el.innerHTML items.map(...); };正确做法是只更新数据让 React 去重渲染// 正确方式状态一变界面自动更新 const [items, setItems] useState([]); const handleAdd (newItem) { setItems((prev) [...prev, newItem]); };状态一变界面跟着变这句话看似简单但它有一个隐含要求你必须在渲染函数里把所有界面分支都描述清楚。比如加载中显示什么、空数据显示什么、报错显示什么都是if分支而不是靠某段代码临时改一下 DOM。这个思维转弯对新手是最大的坎一旦转过来代码的确定性和可预测性会提升一个档次。2.2 副作用组件如何与外部世界同步函数组件在理想状态下是纯的传入同样的状态输出同样的 UI。但真实应用必然要跟外部世界打交道——发请求、写 localStorage、订阅事件、操作 canvas、设置定时器。这些就是副作用。useEffect的核心心智模型不是生命周期钩子而是同步机制。useEffect声明的是当某个依赖变化时我需要和外部世界做一次同步。这句话反过来也成立如果依赖没变那就不需要重新同步。很多新手写useEffect时会把它当成挂载后执行一次的开关依赖数组一填[]就完事了。这在某些场景下确实对但只要业务变复杂依赖数组闭着眼睛写就会埋雷。我常用的判断标准是这个 effect 里用到了哪些外部变量就把它们全部写进依赖数组。要是哪一天你想把它删掉说明这个 effect 根本不该存在。举个例子一个监听窗口大小变化并记录当前尺寸的 effectconst [width, setWidth] useState(window.innerWidth); useEffect(() { const handleResize () setWidth(window.innerWidth); window.addEventListener(resize, handleResize); return () window.removeEventListener(resize, handleResize); }, []);注意这里依赖数组是空的但它内部也没有使用任何外部状态所以[]是合理写法。而下面的写法就有问题// 错误示范遗漏依赖 const [deviceId, setDeviceId] useState(A001); const [data, setData] useState(null); useEffect(() { fetch(/api/devices/${deviceId}).then((res) res.json()).then(setData); }, []); // 这里没写 deviceId切换设备时不会重新请求这种代码在实战场上的表现就是首次挂载正常一切换设备就卡住不更新排查半天才发现依赖遗漏。useEffect的另一个杀手锏是清理函数。定时器、事件监听、取消订阅、中止请求这些都需要在 effect 返回的清理函数里处理。正确写法是同步要彻底清理也要彻底这样组件卸载后才不会留下幽灵回调。3. 状态传递与状态派生数据流动的两条通道3.1 状态传递props、Context、状态提升怎么选React 应用本质上是一棵组件树状态在树上的流动方式直接决定了架构的清爽程度。最基础的方式是 props 逐层传递父组件把状态传给子组件。这套机制简单可靠但层级一深就出问题——你有一个状态存在根组件中间隔着五六层组件每一层都要透传一个config{config}这就是著名的props 穿透。Context 的出现就是为了解决跨层传递的问题。它像一个广播站某个组件在上游提供一个值下游任意层级的组件都能直接读取中间组件完全不需要感知。用得好能极大简化代码但滥用同样致命Context 的 value 一旦变化所有消费它的组件都会重新渲染。如果这个 Context 里塞了一个很大的对象每次setState时对象都变了引用即使其中某个字段没变消费组件也会全部重渲染。所以我的选型标准是组件层级小于两层直接用 props不折腾 Context跨层级超过三层、且数据被大量远端组件共享比如主题、权限、字典、当前用户用 Context如果是某一段业务数据只在少数两个兄弟组件之间共享优先做状态提升把状态挂到最近的共同父级上再用 props 分发下去。状态提升的核心思路是找到真正的数据归属者。两个子组件需要同一份数据那这份数据的家就在它们的共同父节点。父节点管数据子节点管展示和简单交互这是 React 数据流最标准也最不容易出错的模式。3.2 状态派生能算出来的就不要存起来状态派生这句话翻译成大白话就是不要存一个你能算出来的东西。举个例子购物车的商品列表是items你想展示总价。很多人会多写一个totalPrice状态每次商品变化时用setTotalPrice同步更新// 反模式不必要的状态 const [items, setItems] useState([]); const [totalPrice, setTotalPrice] useState(0); const handleAdd (item) { setItems((prev) [...prev, item]); setTotalPrice((prev) prev item.price); };这段代码最大的问题在于一个数据事实items被复制成了两份状态items和totalPrice。用户万一删除了商品漏掉更新totalPrice界面就显示错的总价。这不是极端情况是日常最容易发生的 bug。总价是items的函数它应该被算出来而不是被存起来const totalPrice items.reduce((sum, item) sum item.price, 0);在 React Hooks 里如果这个计算比较轻量直接在渲染函数里算就行。如果计算开销很大或者希望这个计算结果被复用就用useMemo缓存。注意一点useMemo不是为了存值而是为了避免重复计算。const totalPrice useMemo( () items.reduce((sum, item) sum item.price, 0), [items] );这里items一变totalPrice自动重新计算items没变totalPrice直接用缓存。这个模式在实战里太常用了尤其是处理原始数据 - 展示数据的加工链路时比如从传感器数据派生告警等级、从订单数据派生图表指标、从搜索结果派生筛选分组。记住一句话数据源只有一份其他都是派生。这是保持数据一致性的不二法门。4. 实战搭一个设备监控面板把四个概念串起来4.1 需求拆解与数据模型设计我把前面说的概念全部装进一个实际项目里。假设要做一个工业设备监控看板背景是工厂产线上的机器人设备每台设备会持续上报传感器数据比如振动幅度。界面要做三件事展示设备列表、选中某台设备后查看它的实时数据、根据振动值给出告警提示。这与基于数据驱动的轴承故障诊断场景有相似之处——诊断系统根据振动信号判断设备是否异常我们的看板也是根据数据来决定告警等级本质上都是数据驱动的下游应用。先梳理数据模型设备列表[{ id, name }]选中设备 idselectedId单台设备的传感器数据{ vibration }告警阈值配置{ warnThreshold, fatalThreshold }这里vibration是真正的数据源告警等级就是它的派生状态selectedId是用户在页面上的交互状态用于决定右侧面板展示谁阈值配置是全局共享的适合放 Context。组件树设计如下Dashboard最外层容器持有设备列表和选中 id 状态DeviceList左侧列表接收设备列表和选中 id点击时通知父级更新选中 idDeviceDetail右侧详情接收选中 id通过自定义 Hook 加载数据显示实时数据和告警等级AlertConfigPanel阈值配置面板读写 Context 中的阈值这个结构里状态传递用了两种方式设备列表和选中 id 通过 props 状态提升来管理阈值配置通过 Context 跨层级共享。4.2 自定义 Hook 封装数据请求与状态机数据请求是业务里最常见的副作用。我习惯把它封装成自定义 Hook并且把加载中/成功/失败三种状态一次性管理好function useFetchDeviceData(deviceId) { const [data, setData] useState(null); const [loading, setLoading] useState(false); const [error, setError] useState(null); useEffect(() { // 切换设备或首次加载时重置状态 setLoading(true); setError(null); setData(null); const controller new AbortController(); fetch(/api/devices/${deviceId}, { signal: controller.signal }) .then((res) { if (!res.ok) { throw new Error(请求失败${res.status}); } return res.json(); }) .then((json) { setData(json); setLoading(false); }) .catch((err) { if (err.name ! AbortError) { setError(err.message); setLoading(false); } }); // 清理函数组件卸载或 deviceId 变化时取消上一次请求 return () controller.abort(); }, [deviceId]); return { data, loading, error }; }这段代码有几个细节值得说。第一用AbortController取消了上一次请求解决了竞态问题。假设你快速从设备 A 切到设备 BA 的请求比较慢B 的请求先返回随后 A 的请求才返回如果不取消界面就会被 A 的数据覆盖掉明明选的是 B 却显示 A 的数据这是很隐蔽的 bug。第二deviceId一变化就重新请求这正是响应式的体现。用户的操作是改变状态数据的加载是副作用而副作用又反过来通过setData更新状态形成完整闭环。第三自定义 Hook 把这段逻辑从组件里抽出来了。DeviceDetail组件里不用再写任何fetch代码只需要调用const { data, loading, error } useFetchDeviceData(deviceId);组件关心的是在这个状态下渲染什么至于数据怎么来的Hook 管了。4.3 用 useMemo 派生告警等级拿到设备的振动数据之后告警等级不是一个独立的状态它是振动值的派生结果。我直接用useMemo计算function useAlarmLevel(vibration, config) { return useMemo(() { if (vibration null) { return { level: unknown, label: 无数据 }; } if (vibration config.fatalThreshold) { return { level: fatal, label: 严重告警 }; } if (vibration config.warnThreshold) { return { level: warning, label: 预警 }; } return { level: normal, label: 正常 }; }, [vibration, config.warnThreshold, config.fatalThreshold]); }vibration是数据源config是用户配置告警等级完全由这两者推导不存在自己改自己的可能。界面上要显示什么颜色、什么文案全部交给这个派生结果。以后如果想调整告警阈值只需要改配置UI 自动变化。useMemo的依赖数组是[vibration, config.warnThreshold, config.fatalThreshold]我特意写的是具体字段而不是整个config对象。这样当配置里的其他字段变化时不会触发没有必要的重算。依赖数组写得精确一点是 Hooks 性能优化的第一步。还有一个容易忽略的点useMemo里返回的是对象字面量{ level: normal, label: 正常 }每次计算都会创建新对象。如果父组件把useAlarmLevel的返回值传给子组件并当作React.memo子组件的 props那么即使vibration没变只要父组件重新渲染这个派生结果也会看起来变了一个引用导致子组件跟着重渲染。遇到这种情况可以把返回对象打平或者把useMemo的粒度放到单个值上。实战中我一般更倾向于直接返回基本类型的level字符串再用其他方式处理 label。4.4 用 Context 传递阈值配置阈值配置如果只有DeviceDetail自己用根本不需要 Context直接当 prop 传进去就行。但实际项目中阈值配置可能在右上角的设置面板里修改而且多个组件都要读取比如设备列表要显示告警状态的颜色、详情页要算告警等级、设置面板要修改阈值。跨层级共享就轮到 Context 出场了。const AlertConfigContext createContext({ warnThreshold: 4.0, fatalThreshold: 6.0, }); export function AlertConfigProvider({ children }) { const [config, setConfig] useState({ warnThreshold: 4.0, fatalThreshold: 6.0, }); const value useMemo( () ({ config, setConfig }), [config] ); return ( AlertConfigContext.Provider value{value} {children} /AlertConfigContext.Provider ); } export function useAlertConfig() { const ctx useContext(AlertConfigContext); if (!ctx) { throw new Error(useAlertConfig 必须在 AlertConfigProvider 内使用); } return ctx; }这里有三处细节值得抄作业。一是useAlertConfig里加了保护性检查如果在 Provider 外面使用直接抛错。这种自定义 Hook 的做法能让错误在开发阶段就暴露而不是等到深夜里摸不清为什么useContext返回undefined。二是value用useMemo包裹了。这个不是可有可无的优化。AlertConfigProvider每次重渲染会重新创建value对象Context 的所有消费者都会跟着重渲染。用useMemo之后只有config真正变化消费者才会重渲染。在实际业务里这个点对性能影响非常大。三是在 Provider 内部用useState存配置然后由 Provider 负责向下广播。这符合数据驱动的架构——配置数据是源各个消费方的 UI 是下游投影配置一变所有用到它的地方自动更新。消费方用起来非常轻function DeviceAlarmCard({ vibration }) { const { config } useAlertConfig(); const { level, label } useAlarmLevel(vibration, config); if (level fatal) { return div classNamealarm fatal{label}立即停机检查/div; } if (level warning) { return div classNamealarm warning{label}安排维护/div; } return div classNamealarm normal{label}/div; }5. 实战进阶数据驱动思维下的表单、列表与搜索5.1 派生状态之列表筛选用 useMemo 替代事件里算好再 setState列表筛选是前端最典型的需求。反模式是这样的const [keyword, setKeyword] useState(); const [filteredList, setFilteredList] useState(list); // 每次输入关键词都去 setFilteredList const handleSearch (e) { const kw e.target.value; setKeyword(kw); setFilteredList(list.filter((item) item.name.includes(kw))); };问题在哪filteredList和keyword是强关联的数据却维护了两份状态。如果有人在别的地方修改了listfilteredList不会自动更新筛选结果就错了。正确做法是把筛选结果当作派生数据const [keyword, setKeyword] useState(); const filteredList useMemo(() { const kw keyword.trim().toLowerCase(); if (!kw) { return list; } return list.filter((item) item.name.toLowerCase().includes(kw)); }, [list, keyword]);现在list或keyword任何一个变了filteredList都会自动重新计算。事件处理函数里只需要setKeyword(kw)少写了一个setState也少了一类数据不同步的 bug。这个模式可以推广到所有源数据 过滤条件 - 展示数据的场景。5.2 副作用之防抖搜索useEffect setTimeout 的正确写法搜索输入一般都要做防抖避免用户每敲一个字母就发一次请求。防抖本质上是在输入变化这个副作用触发时延迟执行外部操作。用useEffect写防抖比在事件回调里写清晰得多const [keyword, setKeyword] useState(); const [searchResults, setSearchResults] useState([]); useEffect(() { if (!keyword.trim()) { setSearchResults([]); return; } const timer setTimeout(() { fetchSearchResults(keyword).then(setSearchResults); }, 300); // 依赖变化或组件卸载时清除上一个定时器 return () clearTimeout(timer); }, [keyword]);这段代码的逻辑是响应式的关键词每次变化React 先清理上一次的定时器再开启一个新的。用户停顿超过 300ms 后才真正发起搜索。如果用户一直在输入前面的定时器全部被清理请求就永远不会发出去。清理函数在这里不是回收资源这么简单它本身就是防抖机制的一部分。很多新手写防抖会死磕事件回调里的setTimeout和clearTimeout写得又绕又容易漏。换成useEffect之后就变成了一个非常自然的同步-清理-同步循环。我强烈建议有防抖需求的场景都试试这个写法。5.3 状态传递之表单上下文避免 props 地狱大型表单组件是 props 地狱的重灾区。假设表单有十几个字段每个字段可能需要value、onChange、error、touched等 props再拆成子组件后每个都要逐层透传。更麻烦的是很多字段之间存在联动比如省份变了城市选项就要重置。用 Context 管理表单状态是实践中很受用的模式。定一个FormContext管理表单的所有值、错误信息和操作方法const FormContext createContext(null); function FormProvider({ defaultValues, children }) { const [values, setValues] useState(defaultValues); const [errors, setErrors] useState({}); const setFieldValue (name, value) { setValues((prev) ({ ...prev, [name]: value })); // 字段变化时顺便校验一下这个字段 setErrors((prev) ({ ...prev, [name]: validateField(name, value) })); }; const value useMemo(() ({ values, errors, setFieldValue }), [values, errors]); return FormContext.Provider value{value}{children}/FormContext.Provider; } function useFormField(name) { const ctx useContext(FormContext); if (!ctx) { throw new Error(useFormField 必须在 FormProvider 内部使用); } return { value: ctx.values[name], error: ctx.errors[name], setValue: (value) ctx.setFieldValue(name, value), }; }子组件不再需要管状态存在哪、怎么传进来只需要调用const { value, error, setValue } useFormField(city);干净利落。这种集中管理 按需消费的思路其实和数据驱动的思想完全一致表单的值是数据源校验错误是派生结果字段 UI 是投影。所有字段都响应用户输入自动重算自动更新。6. 常见问题与排查技巧实录6.1 无限渲染循环useEffect 里拐了个弯改状态最经典的一个雷是useEffect里调用了会引发重渲染的setState而这个重渲染又导致 effect 重新运行。比如const [user, setUser] useState(null); useEffect(() { setUser({ name: 张三 }); }, [user]);组件挂载 - effect 运行 -setUser-user变化 - 组件重渲染 - effect 再次运行 -setUser再次执行……无限循环。排查思路其实是慢镜头看一遍渲染链路。React DevTools 里有高亮渲染的功能配合打印日志能很快看出哪个 effect 在反复触发。我的经验是useEffect里尽量只做对外世界的同步不要在该 effect 回写它依赖的状态。如果确实需要根据当前状态计算新状态先在事件回调或 reducer 里算好再一次性更新。6.2 依赖数组没写全导致的过期状态stale closure过期闭包问题是所有 Hooks 开发者的老朋友。典型场景const [count, setCount] useState(0); useEffect(() { const timer setInterval(() { setCount(count 1); // 这里的 count 永远是挂载时的 0 }, 1000); return () clearInterval(timer); }, []);结果就是页面上的数字永远停在 1。原因很简单这个 effect 只运行了一次闭包捕获的是第一次渲染时的count也就是 0。之后每次setCount(count 1)都是0 1不会再长了。解决方法有两个思路。第一是函数式更新不依赖闭包里的旧值setCount((prev) prev 1);第二是把count写进依赖数组这样每秒 count 变化后 effect 会重新执行当然你得注意别把定时器搞乱了。更保险的写法是两者结合。我个人的习惯是只要更新操作是基于旧值计算新值一律用函数式更新只有更新的值不依赖旧值时才直接写setValue(newValue)。6.3 竞态问题请求返回顺序错乱接口请求返回的顺序和发起顺序不一定一致。你在列表页快速切换筛选项老的请求可能比新的请求慢结果新界面被老数据覆盖掉。这时候状态和 UI 就错位了。解决竞态有三个层级。第一层是用AbortController取消旧请求就像前面useFetchDeviceData里写的那样。第二层是用一个ignore标志变量useEffect(() { let ignore false; fetchData().then((data) { if (!ignore) { setData(data); } }); return () { ignore true; }; }, [dep]);第三层是用useReducer dispatch在 reducer 层面处理请求标识只接受最新请求的结果。成本从低到高一般项目用前两层就够了。6.4 误把对象当依赖项useMemo/useEffect 的隐蔽陷阱很多人在依赖数组里写[config]但config是每次渲染都新建的对象// 每次渲染 config 都是新对象effect 每次都会执行 function Dashboard({ deviceId }) { const config { warn: 4.0, fatal: 6.0 }; useEffect(() { console.log(config 变化了); }, [config]); }这里的config是字面量对象React 用Object.is比较依赖新对象和旧对象引用不同所以 effect 每次渲染都执行。别小看这个问题它会导致接口疯狂请求、无限重试、性能断崖。解决办法是依赖项换成config.warn、config.fatal这种具体的基本类型或者用useMemo缓存对象保证引用稳定如果对象来自 props那就要看父组件有没有用useMemo/useCallback保证引用稳定了。排查技巧很简单在 effect 里console.log依赖值看上一次和这一次的打印是否真的不同。如果值一样但 effect 还是频繁触发基本就是引用问题。6.5 自定义 Hook 的三大优势与封装标准站在项目维度看自定义 Hook 带来的收益非常明显。首先是逻辑复用——数据请求、表单校验、权限判断、分页逻辑这些在不同组件里反复出现的逻辑抽成 Hook 后一次封装到处使用。其次是职责清晰——组件只负责渲染和事件触发复杂逻辑被藏进 Hook一个很长的组件可以被拆成一段语义明确的代码。最后是测试友好——Hook 本质是一个普通函数可以进行单独测试不依赖 DOM。但是我见过不少过度封装。判断一个逻辑要不要抽成 Hook可以参考三个标准这个逻辑是否在多个组件里重复出现这个逻辑是否独立于组件 UI可以自成一个状态单元它是否有一段完整的生命周期加载、更新、清理如果答案都是是就值得抽。如果只是某一段代码在两个组件里长得像但生命周期完全不同硬抽反而两头难受。好的 Hook 应该像乐高积木小而精组合起来能搭出各种形状坏的 Hook 是面条超级长一个函数管二十个状态改了但拆不动。我踩过最深的坑是抽 Hook 时不注意返回值的稳定性。比如 Hook 返回一个数组包含两个方法function useCounter(initial 0) { const [count, setCount] useState(initial); return { count, increment: () setCount((c) c 1), decrement: () setCount((c) c - 1), }; }每次渲染都会创建新的increment函数对象。如果这个 Hook 被用在React.memo的子组件里并把increment作为 prop 传递那么子组件的 memo 优化就失效了。解决方法是给方法包一层useCallbackfunction useCounter(initial 0) { const [count, setCount] useState(initial); const increment useCallback(() setCount((c) c 1), []); const decrement useCallback(() setCount((c) c - 1), []); return { count, increment, decrement }; }项目规模越大这种稳定引用的重要性越突出。性能问题通常不是某一次渲染慢而是大量没有意义的子组件重渲染累积出来的。把这些细节处理好组件树在复杂度上去之后依然能保持敏捷。从最初类组件的this.setState到现在的各类 HooksReact 的演进本质上是越来越接近状态驱动一切的模型。我自己从切到 Hooks 之后最大的变化不是代码风格而是思维习惯拿到一个需求第一反应变成了这个页面的数据源是谁哪些数据是派生出来的哪些副作用要响应数据变化当这四个问题想清楚了代码怎么写基本就定了。最后再分享一个小技巧复杂业务里给每个 useState 和 useEffect 写一行注释说明这个状态代表什么以及这个副作用在同步什么。等过一个月你再回来看这段代码会觉得当时留了一盏灯。我实践下来这比任何规范文档都管用。