diff --git a/src/useList/__tests__/useList.test.ts b/src/useList/__tests__/useList.test.ts index f9b554219..8e9bc701f 100644 --- a/src/useList/__tests__/useList.test.ts +++ b/src/useList/__tests__/useList.test.ts @@ -67,6 +67,84 @@ 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 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/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..061d0606d 100644 --- a/src/useList/index.ts +++ b/src/useList/index.ts @@ -1,5 +1,28 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { merge } from 'lodash-es'; +import { isPlainObject, merge } from 'lodash-es'; + +const mergeParams = ( + previous: 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, merged) + : nextValue; + }); + + return result; +}; export type Fetcher = (params: P) => Promise<{ data: T[]; total: number }>; @@ -91,7 +114,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) {