react-use 的 useUpsert Hook 全面解析:列表 upsert 操作与向 useList 的迁移指南
react-use 的 useUpsert Hook 全面解析列表 upsert 操作与向 useList 的迁移指南【免费下载链接】react-useReact Hooks — 项目地址: https://gitcode.com/gh_mirrors/re/react-use本文聚焦 react-use当前仓库版本 17.6.1中已标记为DEPRECATED的useUpsertHook。它以 useList 为基底额外提供upsert更新或插入能力用于在列表中按自定义比较函数完成存在则更新、不存在则追加的原子操作。读完本文你将掌握useUpsert的完整 API 与内部实现原理、如何用useList的upsertaction 平滑替换它以及对应的测试验证方式。一、背景为什么需要 useUpsert在 React 组件中维护数组状态时最常见的两个诉求是修改已有元素和新增元素。传统写法需要先findIndex判断元素是否存在再决定调用更新还是追加逻辑代码既重复又容易出错。useUpsert正是为这种场景而生它是 useList 的超集Superset在保留useList全部能力的基础上新增了一个upsert方法——传入一个比较函数predicate和新元素若列表中已有满足 predicate 的元素则用新元素替换它更新若没有则把新元素追加到列表末尾插入。⚠️重要提醒在 react-use 当前版本中useUpsert已被官方标记为DEPRECATED已弃用文档与源码都明确建议直接使用useListHook 自带的upsertaction 替代它。详见 docs/useUpsert.md 顶部声明与 src/useUpsert.ts 中的deprecated注解。建议新代码直接使用useList本文也会给出完整的迁移示例。二、API 签名与参数说明从 src/useUpsert.ts 的源码可以看出useUpsert是一个泛型函数export default function useUpsertT( predicate: (a: T, b: T) boolean, initialList: IHookStateInitActionT[] [] ): [T[], UpsertListActionsT]2.1 参数详解参数类型默认值说明predicate(a: T, b: T) boolean必填比较函数接收列表中的元素a与新元素b返回true表示二者相等应被视作同一条记录initialListIHookStateInitActionT[][]初始列表可以传数组本身也可以传返回数组的函数惰性初始化其中IHookStateInitActionS定义在 src/misc/hookState.tsexport type IHookStateInitialSetterS () S; export type IHookStateInitActionS S | IHookStateInitialSetterS;也就是说初始值既可以是DemoType[]也可以是一个() DemoType[]函数——传入函数时会在初始化阶段调用一次惰性求值这在初始数据计算成本较高时很有用。2.2 返回值返回一个元组[list, actions]listT[]当前列表数据实时反映在每次渲染中actionsUpsertListActionsT继承自useList的ListActionsT但剔除了其中的upsert字段因为useUpsert版本签名不同并新增了自己的upsert。从源码 src/useUpsert.ts 可见其类型定义export interface UpsertListActionsT extends OmitListActionsT, upsert { upsert: (newItem: T) void; }注意差异点useList的upsert签名是upsert(predicate, newItem)需要每次显式传入比较函数useUpsert的upsert签名是upsert(newItem)比较函数已在 Hook 调用时固化这是它唯一的便利性所在。actions中其余方法set、push、updateAt、insertAt、update、updateFirst、sort、filter、removeAt、remove、clear、reset与 src/useList.ts 中定义的ListActionsT完全一致完整参考可查阅 docs/useList.md 的 Reference 小节。三、使用示例完整可运行以下是文档 docs/useUpsert.md 中的完整示例也是仓库内 Storybook 演示见 stories/useUpsert.story.tsx所用的代码——它模拟了一个可编辑的待办列表输入框实时 upsert 更新当前行点击按钮新增空行Remove按钮按索引删除Reset清空列表import {useUpsert} from react-use; interface DemoType { id: string; text: string; } const initialItems: DemoType[] [ { id: 1, text: Sample }, { id: 2, text: }, ]; const Demo () { const comparisonFunction (a: DemoType, b: DemoType) { return a.id b.id; }; const [list, { set, upsert, remove }] useUpsert(comparisonFunction, initialItems); return ( div style{{ display: inline-flex, flexDirection: column }} {list.map((item: DemoType, index: number) ( div key{item.id} input value{item.text} onChange{e upsert({ ...item, text: e.target.value })} / button onClick{() remove(index)}Remove/button /div ))} button onClick{() upsert({ id: (list.length 1).toString(), text: })}Add item/button button onClick{() set([])}Reset/button /div ); };3.1 示例行为推演修改第 1 行输入框upsert({ id: 1, text: 新文本 })→ predicate 命中id 1列表中该项被整体替换为传入的新对象点击 Add itemupsert({ id: 3, text: })→ predicate 遍历后无命中新对象被push 到末尾点击 Removeremove(index)→ 按索引删除注意remove本身在useList中也被标记为弃用推荐改用removeAt见 src/useList.ts点击 Resetset([])→ 用空数组整体替换列表。关键设计点upsert 是整对象替换而非字段合并。示例中upsert({ ...item, text: e.target.value })显式展开了原对象再覆盖text字段如果你直接传入只含id和text之外部分字段的对象命中后该条记录会被整个替换成新对象其余字段将丢失。这是使用时最容易踩的坑。四、源码级原理useUpsert 如何实现useUpsert的完整实现非常精简src/useUpsert.ts 全文仅 26 行本质是对useList的一层薄封装export default function useUpsertT( predicate: (a: T, b: T) boolean, initialList: IHookStateInitActionT[] [] ): [T[], UpsertListActionsT] { const [list, listActions] useList(initialList); return [ list, { ...listActions, upsert: (newItem: T) { listActions.upsert(predicate, newItem); }, } as UpsertListActionsT, ]; }4.1 底层useList.upsert的实现逻辑真正干活的upsert位于 src/useList.tsupsert: (predicate: (a: T, b: T) boolean, newItem: T) { const index list.current.findIndex((item) predicate(item, newItem)); index 0 ? actions.updateAt(index, newItem) : actions.push(newItem); },执行流程拆解用list.current.findIndex(...)在当前列表存储在useRef中里查找第一个满足predicate(item, newItem)的元素下标若index 0已存在调用updateAt(index, newItem)通过curr.slice()复制数组后原地替换下标位置元素返回新数组若index -1不存在调用push(newItem)通过curr.concat(items)生成追加后的新数组。useUpsert所做的工作就是把这个二元函数柯里化式地预绑定了 predicate调用useUpsert(predicate)之后每次只需upsert(newItem)而useList则要求每次调用都显式传predicate。4.2 列表数据与重渲染机制useList把列表本体存放在useRef中src/useList.ts并用 src/useUpdate.ts 提供的useUpdate()内部是一个useReducer计数器强制触发重渲染。所有修改动作都遵循不可变更新原则先slice()复制再修改副本最后通过set写入新数组——这正是测试中断言not.toBe(testItems)引用不相等能通过的原因。此外actions对象通过useMemo只创建一次依赖数组为空src/useList.ts因此 actions 及其方法在渲染间保持引用稳定可以安全地放进useEffect依赖数组或直接下发给子组件不会引发额外的重复订阅/重复执行。4.3 从源码结构可以推断的细节useUpsert的返回值第二项通过对象展开{ ...listActions, upsert }组合而成因此useList的全部 action 在useUpsert中同样可用由于list直接透传useList的返回值useUpsert自身的实现中并没有额外的状态管理新增的只有闭包化的 predicate 绑定逻辑类型层面通过as UpsertListActionsT断言完成从ListActionsT到去掉upsert字段的转换因为运行时对象里只存在useUpsert版签名的upsert。五、测试验证upsert 的行为契约仓库为useUpsert提供了专门的单元测试 tests/useUpsert.test.ts它基于testing-library/react-hooks的renderHook编写将上述行为契约固化成了三条核心断言1. 初始化initialization列表内容与传入的初始值一致expect(list).toEqual(testItems)返回对象中upsert是一个函数expect(utils.upsert).toBeInstanceOf(Function)。2. 插入新元素upserting a new item——传入id: 3列表中不存在新元素出现在列表中expect(result.current[0]).toContain(newItem)操作是不可变的返回了全新数组expect(result.current[0]).not.toBe(testItems)。3. 更新已存在元素upserting an existing item——传入{ id: 2, text: 4 }id: 2已存在列表长度不变expect(updatedList).toHaveLength(testItems.length)旧对象被替换为新对象expect(updatedList).toContain(newItem)同样保持不可变性expect(updatedList).not.toBe(testItems)。这三组用例完整覆盖了 upsert 的两条分支路径命中→更新、未命中→插入以及不可变更新的核心承诺。测试中的比较函数itemsAreEqual与文档示例一致均以id作为唯一性判据。运行方式yarn test对应 package.json 中的test: jest --maxWorkers 2也可用yarn test:watch进入监听模式。六、迁移指南从 useUpsert 平滑切换到 useList既然官方已弃用useUpsert新代码应直接使用useList。迁移非常机械——只需把比较函数从 Hook 参数位置移动到每次upsert调用的第一个参数迁移前useUpsertconst [list, { set, upsert, remove }] useUpsert( (a, b) a.id b.id, initialItems ); // 调用时无需传比较函数 upsert({ ...item, text: e.target.value }); upsert({ id: 3, text: });迁移后useListconst [list, { set, upsert, remove }] useList(initialItems); // 每次调用需显式传入比较函数 upsert((a, b) a.id b.id, { ...item, text: e.target.value }); upsert((a, b) a.id b.id, { id: 3, text: });迁移时还需注意两点顺手替换弃用方法remove在useList中同样被标记为弃用src/useList.ts它目前只是removeAt的别名建议迁移时一并改为removeAt(index)useUpsert的导出仍然保留尽管已弃用它仍从入口 src/index.ts 正常导出export { default as useUpsert } from ./useUpsert;老代码在未升级前仍可继续使用只是 IDE 会基于deprecated注解给出弃用提示。七、相关资源Hook 文档docs/useUpsert.md、docs/useList.md源码实现src/useUpsert.ts、src/useList.ts、src/misc/hookState.ts、src/useUpdate.ts测试用例tests/useUpsert.test.ts交互演示stories/useUpsert.story.tsx可通过yarn storybook在本地 Storybook 中运行体验总结useUpsert是 react-use 为按唯一标识更新或插入列表元素这一高频场景提供的便捷封装其核心价值在于将比较函数在 Hook 层固化、简化调用签名而其底层行为完全委托给useList。鉴于官方已将其标记为弃用理解它的最好方式就是掌握它背后的useList.upsert——那也正是你下一步应该使用的 API。【免费下载链接】react-useReact Hooks — 项目地址: https://gitcode.com/gh_mirrors/re/react-use创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →