From da0f03b118b9a0852fda296c3466a3d7199dec56 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Sat, 10 Oct 2026 14:50:33 +0800 Subject: [PATCH 1/2] fix(uselist): correct useList parameter merging fix #632 --- src/useList/__tests__/useList.test.ts | 60 +++++++++++++++++++++++++++ src/useList/demos/mutate.tsx | 4 +- src/useList/demos/mutateOptions.tsx | 4 +- src/useList/index.md | 2 +- src/useList/index.ts | 22 +++++++++- 5 files changed, 83 insertions(+), 9 deletions(-) diff --git a/src/useList/__tests__/useList.test.ts b/src/useList/__tests__/useList.test.ts index f9b554219..f1dd8fce1 100644 --- a/src/useList/__tests__/useList.test.ts +++ b/src/useList/__tests__/useList.test.ts @@ -67,6 +67,66 @@ describe('Test useList hook', () => { expect(result.current.params).toEqual(expect.objectContaining({ search: 'test' })); }); + it('Should merge object params while replacing arrays and nullable values', () => { + type Params = { + current: number; + filters: number[]; + search?: string; + nullable: string | null; + nested: { + first?: number; + second?: number; + filters?: number[]; + search?: string; + }; + }; + const fetcher = jest.fn().mockResolvedValue({ total: 0, data: [] }); + const { result } = renderHook(() => + useList( + fetcher, + { + current: 1, + filters: [1, 2, 3], + search: 'test', + nullable: 'value', + nested: { + first: 1, + second: 2, + filters: [1, 2, 3], + search: 'nested', + }, + } as Params, + { immediate: false } + ) + ); + + act(() => { + result.current.mutate( + { + filters: [4, 5], + search: undefined, + nullable: null, + nested: { first: 3, filters: [4, 5], search: undefined }, + }, + { revalidate: false } + ); + }); + + expect(result.current.params).toEqual({ + current: 1, + filters: [4, 5], + search: undefined, + nullable: null, + nested: { + first: 3, + second: 2, + filters: [4, 5], + search: undefined, + }, + total: 0, + }); + }); + it('Should support get data with current params', () => { const fetcher = jest.fn().mockResolvedValue({ total: 1, diff --git a/src/useList/demos/mutate.tsx b/src/useList/demos/mutate.tsx index 0df448fc1..839a54a7e 100644 --- a/src/useList/demos/mutate.tsx +++ b/src/useList/demos/mutate.tsx @@ -32,9 +32,7 @@ export default () => { const handleReset = async () => { form.resetFields(); const values = await form.validateFields(); - // 当传入值有 undefined 的时候,采用 functional 的写法。 - // 因为底层使用的 lodash 的 merge,采用赋值写法不会对 undefined 做合并 - mutate((pre) => ({ ...pre, ...values })); + mutate({ ...values }); }; return ( diff --git a/src/useList/demos/mutateOptions.tsx b/src/useList/demos/mutateOptions.tsx index 9ae4bfacd..e3a7bed3e 100644 --- a/src/useList/demos/mutateOptions.tsx +++ b/src/useList/demos/mutateOptions.tsx @@ -32,9 +32,7 @@ export default () => { const handleReset = async () => { form.resetFields(); const values = await form.validateFields(); - // 当传入值有 undefined 的时候,采用 functional 的写法。 - // 因为底层使用的 lodash 的 merge,采用赋值写法不会对 undefined 做合并 - mutate((pre) => ({ ...pre, ...values }), { revalidate, clearData }); + mutate({ ...values }, { revalidate, clearData }); }; const [revalidate, setRevalidate] = useState(true); const [clearData, setClearData] = useState(true); diff --git a/src/useList/index.md b/src/useList/index.md index 1c3c2c7be..5c64a66f2 100644 --- a/src/useList/index.md +++ b/src/useList/index.md @@ -15,7 +15,7 @@ toc: content - + diff --git a/src/useList/index.ts b/src/useList/index.ts index 3b923899e..69dcd7d26 100644 --- a/src/useList/index.ts +++ b/src/useList/index.ts @@ -1,5 +1,23 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { merge } from 'lodash-es'; +import { isPlainObject, merge } from 'lodash-es'; + +const mergeParams = ( + previous: Record, + next: Record +): Record => { + const result = { ...previous }; + + Object.keys(next).forEach((key) => { + const previousValue = previous[key]; + const nextValue = next[key]; + result[key] = + isPlainObject(previousValue) && isPlainObject(nextValue) + ? mergeParams(previousValue, nextValue) + : nextValue; + }); + + return result; +}; export type Fetcher = (params: P) => Promise<{ data: T[]; total: number }>; @@ -91,7 +109,7 @@ export default function useList, P extends Record< }; const nextOptions = merge(defaultOptions, options); - const tmp = typeof next === 'function' ? next(params) : { ...merge({}, params, next) }; + const tmp = typeof next === 'function' ? next(params) : (mergeParams(params, next) as P); setParams(tmp); if (nextOptions.revalidate) { From 275047327a764869b5558f29d4d4f696b9cf4729 Mon Sep 17 00:00:00 2001 From: LuckyFBB <976060700@qq.com> Date: Sat, 10 Oct 2026 17:34:33 +0800 Subject: [PATCH 2/2] fix(uselist): support circular references in mergeParams --- src/useList/__tests__/useList.test.ts | 18 ++++++++++++++++++ src/useList/index.ts | 9 +++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/useList/__tests__/useList.test.ts b/src/useList/__tests__/useList.test.ts index f1dd8fce1..8e9bc701f 100644 --- a/src/useList/__tests__/useList.test.ts +++ b/src/useList/__tests__/useList.test.ts @@ -127,6 +127,24 @@ describe('Test useList hook', () => { }); }); + it('Should preserve circular references when merging object params', () => { + const fetcher = jest.fn().mockResolvedValue({ total: 0, data: [] }); + const previous: Record = { value: 'previous' }; + const next: Record = { value: 'next' }; + previous.self = previous; + next.self = next; + const { result } = renderHook(() => + useList(fetcher, { nested: previous }, { immediate: false }) + ); + + act(() => { + result.current.mutate({ nested: next }, { revalidate: false }); + }); + + expect(result.current.params.nested.value).toBe('next'); + expect(result.current.params.nested.self).toBe(result.current.params.nested); + }); + it('Should support get data with current params', () => { const fetcher = jest.fn().mockResolvedValue({ total: 1, diff --git a/src/useList/index.ts b/src/useList/index.ts index 69dcd7d26..061d0606d 100644 --- a/src/useList/index.ts +++ b/src/useList/index.ts @@ -3,16 +3,21 @@ import { isPlainObject, merge } from 'lodash-es'; const mergeParams = ( previous: Record, - next: Record + next: Record, + merged = new WeakMap>() ): Record => { + const cached = merged.get(next); + if (cached) return cached; + const result = { ...previous }; + merged.set(next, result); Object.keys(next).forEach((key) => { const previousValue = previous[key]; const nextValue = next[key]; result[key] = isPlainObject(previousValue) && isPlainObject(nextValue) - ? mergeParams(previousValue, nextValue) + ? mergeParams(previousValue, nextValue, merged) : nextValue; });