diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index d956bb1a91..92873b703a 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -31,8 +31,9 @@ import type { QueryActionCreatorResult, MutationActionCreatorResult, } from '@reduxjs/toolkit/dist/query/core/buildInitiate' +import type { SerializeQueryArgs } from '@reduxjs/toolkit/dist/query/defaultSerializeQueryArgs' import { shallowEqual } from 'react-redux' -import type { Api } from '@reduxjs/toolkit/dist/query/apiTypes' +import type { Api, ApiContext } from '@reduxjs/toolkit/dist/query/apiTypes' import type { Id, NoInfer, @@ -45,9 +46,10 @@ import type { PrefetchOptions, } from '@reduxjs/toolkit/dist/query/core/module' import type { ReactHooksModuleOptions } from './module' -import { useShallowStableValue } from './useShallowStableValue' +import { useStableQueryArgs } from './useSerializedStableValue' import type { UninitializedValue } from './constants' import { UNINITIALIZED_VALUE } from './constants' +import { useShallowStableValue } from './useShallowStableValue' // Copy-pasted from React-Redux export const useIsomorphicLayoutEffect = @@ -464,9 +466,13 @@ type GenericPrefetchThunk = ( export function buildHooks({ api, moduleOptions: { batch, useDispatch, useSelector, useStore }, + serializeQueryArgs, + context, }: { api: Api moduleOptions: Required + serializeQueryArgs: SerializeQueryArgs + context: ApiContext }) { return { buildQueryHooks, buildMutationHook, usePrefetch } @@ -505,7 +511,12 @@ export function buildHooks({ Definitions > const dispatch = useDispatch>() - const stableArg = useShallowStableValue(skip ? skipToken : arg) + const stableArg = useStableQueryArgs( + skip ? skipToken : arg, + serializeQueryArgs, + context.endpointDefinitions[name], + name + ) const stableSubscriptionOptions = useShallowStableValue({ refetchOnReconnect, refetchOnFocus, @@ -640,7 +651,12 @@ export function buildHooks({ QueryDefinition, Definitions > - const stableArg = useShallowStableValue(skip ? skipToken : arg) + const stableArg = useStableQueryArgs( + skip ? skipToken : arg, + serializeQueryArgs, + context.endpointDefinitions[name], + name + ) const lastValue = useRef() diff --git a/packages/toolkit/src/query/react/module.ts b/packages/toolkit/src/query/react/module.ts index 89459f4f36..18f99ac80c 100644 --- a/packages/toolkit/src/query/react/module.ts +++ b/packages/toolkit/src/query/react/module.ts @@ -109,7 +109,7 @@ export const reactHooksModule = ({ useStore = rrUseStore, }: ReactHooksModuleOptions = {}): Module => ({ name: reactHooksModuleName, - init(api, options, context) { + init(api, { serializeQueryArgs }, context) { const anyApi = api as any as Api< any, Record, @@ -120,6 +120,8 @@ export const reactHooksModule = ({ const { buildQueryHooks, buildMutationHook, usePrefetch } = buildHooks({ api, moduleOptions: { batch, useDispatch, useSelector, useStore }, + serializeQueryArgs, + context, }) safeAssign(anyApi, { usePrefetch }) safeAssign(context, { batch }) diff --git a/packages/toolkit/src/query/react/useSerializedStableValue.ts b/packages/toolkit/src/query/react/useSerializedStableValue.ts new file mode 100644 index 0000000000..163f63eecd --- /dev/null +++ b/packages/toolkit/src/query/react/useSerializedStableValue.ts @@ -0,0 +1,31 @@ +import { useEffect, useRef, useMemo } from 'react' +import type { SerializeQueryArgs } from '@reduxjs/toolkit/dist/query/defaultSerializeQueryArgs' +import type { EndpointDefinition } from '@reduxjs/toolkit/dist/query/endpointDefinitions' + +export function useStableQueryArgs( + queryArgs: T, + serialize: SerializeQueryArgs, + endpointDefinition: EndpointDefinition, + endpointName: string +) { + const incoming = useMemo( + () => ({ + queryArgs, + serialized: + typeof queryArgs == 'object' + ? serialize({ queryArgs, endpointDefinition, endpointName }) + : queryArgs, + }), + [queryArgs, serialize, endpointDefinition, endpointName] + ) + const cache = useRef(incoming) + useEffect(() => { + if (cache.current.serialized !== incoming.serialized) { + cache.current = incoming + } + }, [incoming]) + + return cache.current.serialized === incoming.serialized + ? cache.current.queryArgs + : queryArgs +} diff --git a/packages/toolkit/src/query/tests/buildHooks.test.tsx b/packages/toolkit/src/query/tests/buildHooks.test.tsx index 18ea3ee57c..3047ae8b24 100644 --- a/packages/toolkit/src/query/tests/buildHooks.test.tsx +++ b/packages/toolkit/src/query/tests/buildHooks.test.tsx @@ -458,18 +458,19 @@ describe('hooks tests', () => { let { unmount } = render(, { wrapper: storeRef.wrapper }) + expect(screen.getByTestId('isFetching').textContent).toBe('false') + // skipped queries do nothing by default, so we need to toggle that to get a cached result fireEvent.click(screen.getByText('change skip')) await waitFor(() => expect(screen.getByTestId('isFetching').textContent).toBe('true') ) - await waitFor(() => - expect(screen.getByTestId('isFetching').textContent).toBe('false') - ) - await waitFor(() => + + await waitFor(() => { expect(screen.getByTestId('amount').textContent).toBe('1') - ) + expect(screen.getByTestId('isFetching').textContent).toBe('false') + }) unmount() diff --git a/packages/toolkit/src/query/tests/buildThunks.test.tsx b/packages/toolkit/src/query/tests/buildThunks.test.tsx index 7ba0aaf35e..f5997ca94d 100644 --- a/packages/toolkit/src/query/tests/buildThunks.test.tsx +++ b/packages/toolkit/src/query/tests/buildThunks.test.tsx @@ -156,12 +156,14 @@ describe('re-triggering behavior on arg change', () => { } }) - test('re-trigger every time on deeper value changes', async () => { + test('re-triggers every time on deeper value changes', async () => { + const name = 'Tim' + const { result, rerender, waitForNextUpdate } = renderHook( (props) => getUser.useQuery(props), { wrapper: withProvider(store), - initialProps: { person: { name: 'Tim' } }, + initialProps: { person: { name } }, } ) @@ -171,7 +173,7 @@ describe('re-triggering behavior on arg change', () => { expect(spy).toHaveBeenCalledTimes(1) for (let x = 1; x < 3; x++) { - rerender({ person: { name: 'Tim' } }) + rerender({ person: { name: name + x } }) // @ts-ignore while (result.current.status === 'pending') { await waitForNextUpdate()