ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

react-use 之 useGetSet:用 getter 函数规避闭包过期值(Stale Closure)陷阱的 React 状态 Hook

react-use 之 useGetSet:用 getter 函数规避闭包过期值(Stale Closure)陷阱的 React 状态 Hook react-use 之 useGetSet用 getter 函数规避闭包过期值Stale Closure陷阱的 React 状态 Hook【免费下载链接】react-useReact Hooks — 项目地址: https://gitcode.com/gh_mirrors/re/react-useuseGetSet是 react-use 提供的一个 React 状态 Hook它不直接返回原始状态值而是返回一个get取值函数与一个set设值函数。这一设计专为状态在嵌套函数如setTimeout回调、事件处理器中被读取的场景打造能够从根源上规避useState因闭包捕获过期值stale closure而引发的计数器错乱等隐蔽 Bug。读完本文你将掌握useGetSet的完整用法、与useState的对比差异、其源码级实现原理以及它在延迟累加这类典型异步场景中的实战价值。核心用法get 代替状态值本身useGetSet的调用方式与useState相似接受一个初始值但返回的是两个函数import {useGetSet} from react-use; const Demo () { const [get, set] useGetSet(0); const onClick () { setTimeout(() { set(get() 1) }, 1_000); }; return ( button onClick{onClick}Clicked: {get()}/button ); };几个关键点get是一个函数调用get()才能拿到当前最新的状态值因此渲染时也必须写{get()}而非{cnt}set是一个设值函数用法与useState的 setter 一致在setTimeout、setInterval、Promise 回调、事件处理器等嵌套函数中读取状态时直接调用get()读取到的一定是那一刻的最新值而不是创建回调时闭包捕获的旧值。该示例同样以可运行的 Storybook 故事形式存在于仓库中stories/useGetSet.story.tsx并配有独立测试用例验证其在嵌套函数中的行为tests/useGetSet.test.ts。为什么需要它useState的闭包过期值陷阱如果采用useState的朴素写法实现同样的每次点击 1 秒后自增快速连点多次时计数器会出问题const DemoWrong () { const [cnt, set] useState(0); const onClick () { setTimeout(() { set(cnt 1) }, 1_000); }; return ( button onClick{onClick}Clicked: {cnt}/button ); };问题根源在于闭包捕获closure captureonClick内setTimeout的回调是在创建的那一刻捕获了当时的cnt快照。快速连点 3 次时3 个定时器回调捕获到的cnt都是 01 秒后各自执行set(0 1)最终计数器只会停在 1而不是期望的 3。反观useGetSet版本回调里写的是set(get() 1)get()在定时器真正触发时才去读取最新状态因此 3 次回调依次读到 0、1、2最终结果是正确的 3。这一行为被仓库测试 tests/useGetSet.test.ts 严格验证模拟 3 次点击后jest.runAllTimers()快进所有定时器断言get()返回 3。源码实现useRef 存值 useUpdate 强制渲染useGetSet的实现非常精简src/useGetSet.tsimport { Dispatch, useMemo, useRef } from react; import useUpdate from ./useUpdate; import { IHookStateInitAction, IHookStateSetAction, resolveHookState } from ./misc/hookState; export default function useGetSetS( initialState: IHookStateInitActionS ): [get: () S, set: DispatchIHookStateSetActionS] { const state useRef(resolveHookState(initialState)); const update useUpdate(); return useMemo( () [ () state.current as S, (newState: IHookStateSetActionS) { state.current resolveHookState(newState, state.current); update(); }, ], [] ); }逐行拆解其工作原理useRef保存最新状态state是一个 refget()直接返回state.current。ref 对象在整个组件生命周期内保持稳定且state.current是实时可读的——这正是它不怕闭包过期值的根本原因。useMemo保证 get/set 身份稳定get和set被包裹在依赖为[]的useMemo中组件每次渲染都返回同一对函数引用非常适合传给子组件、加入 effect 依赖数组不会因函数身份变化引发额外重渲染或重复订阅。set写值并强制渲染set先将新值写入state.current再调用update()触发一次重渲染从而让 UI 与内部状态保持同步。useUpdate基于 useReducer 的强制更新useGetSet依赖的update来自 src/useUpdate.tsimport { useReducer } from react; const updateReducer (num: number): number (num 1) % 1_000_000; export default function useUpdate(): () void { const [, update] useReducer(updateReducer, 0); return update; }它通过useReducer每次把计数加 1取模 1_000_000 防止溢出来触发一次无状态变更的强制渲染。也就是说useGetSet的状态本体其实存放在 ref 里React 的 state 机制只负责充当渲染信号灯。参数类型与函数式更新IHookState 类型体系从类型签名可以看出useGetSet的初始值与更新值都支持函数形式。这些类型定义在 src/misc/hookState.tsexport type IHookStateInitialSetterS () S; export type IHookStateInitActionS S | IHookStateInitialSetterS; export type IHookStateSetterS ((prevState: S) S) | (() S); export type IHookStateSetActionS S | IHookStateSetterS;初始值IHookStateInitActionS可以传一个普通值也可以传一个懒初始化函数() S更新值IHookStateSetActionS可以传一个新值也可以传一个函数函数会接收到当前的state.current作为参数。无论传入的是值还是函数最终都会经过resolveHookState统一解析src/misc/hookState.tsexport function resolveHookStateS, C extends S( nextState: IHookStateResolvableS, currentState?: C ): S { if (typeof nextState function) { return nextState.length ? (nextState as Function)(currentState) : (nextState as Function)(); } return nextState; }解析规则值得注意传入函数时若函数声明了参数length 0则以当前状态为参数调用它实现函数式更新set(prev prev 1)若函数不声明参数length 0则被当作懒初始化/无参工厂函数直接调用传入普通值时原样返回。因此useGetSet的set完全支持set(1)、set(prev prev 1)两种写法与useState的函数式更新体验一致。完整可运行示例综合以上要点一个既演示嵌套函数取值、又演示函数式更新的完整组件如下import React from react; import {useGetSet} from react-use; const Counter () { const [get, set] useGetSet(0); // 异步延迟累加规避闭包过期值 const delayedIncrement () { setTimeout(() { set(get() 1); }, 1_000); }; // 函数式更新基于最新值翻倍 const double () set(prev prev * 2); return ( div p当前值{get()}/p button onClick{delayedIncrement}1 秒后 1/button button onClick{double}翻倍/button /div ); };测试验证行为即规格仓库为useGetSet提供了 4 组测试tests/useGetSet.test.ts可作为行为规格参考测试场景断言内容初始化get、set均为函数L10-L16读取当前值get()返回初始值fooL18-L25设置新值act(() set(bar))后get()返回barL27-L35嵌套函数场景连点 3 次、runAllTimers()快进后值为 3L41-L64最后一组测试正是文档示例的自动化版本它用jest.useFakeTimers()模拟 1 秒延迟连续触发 3 次onClick再快进所有定时器最终断言get()等于 3从测试层面坐实了useGetSet对闭包过期值问题的解决能力。使用注意事项渲染中读取useGetSet的set内部通过useUpdate强制重渲染属于无状态变更的渲染若在渲染阶段直接调用set需遵守 React 渲染约束常规用法事件回调/异步回调中调用不受影响get只在需要时调用get是函数而非值JSX 中必须写成{get()}它不会建立响应式依赖适合在回调中按需取最新值与useGetSetState的区别react-use 还提供 useGetSetState文档见 docs/useGetSetState.md后者针对对象状态合并更新场景而useGetSet面向任意类型的单一状态两者可按需选用导出路径useGetSet已从库入口统一导出src/index.ts直接import {useGetSet} from react-use即可无需额外配置。小结useGetSet用一个极简的ref 存值 强制渲染方案化解了 React Hooks 生态中高频出现的闭包过期值问题凡是在异步回调、定时器、事件监听等嵌套函数里需要读写状态的场景用get()替代直接的状态引用就能以最小的心智成本写出行为正确、可预测的代码。其函数身份稳定useMemo缓存、支持函数式更新、有测试与 Storybook 故事双重佐证是 react-use 状态类 Hooks 中兼具实用性与教学价值的典范实现。【免费下载链接】react-useReact Hooks — 项目地址: https://gitcode.com/gh_mirrors/re/react-use创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表