From a41438c7cb428020bcf92fcd8cfa4aa48b5b818a Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Sat, 13 Nov 2021 14:13:19 +0100 Subject: [PATCH 1/5] move queryStatePreSelector into buildHooks inner scope --- .../toolkit/src/query/react/buildHooks.ts | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index a5384380e5..5a9029bb82 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -460,33 +460,6 @@ export type MutationTrigger> = const defaultQueryStateSelector: QueryStateSelector = (x) => x const defaultMutationStateSelector: MutationStateSelector = (x) => x -const queryStatePreSelector = ( - currentState: QueryResultSelectorResult, - lastResult: UseQueryStateDefaultResult -): UseQueryStateDefaultResult => { - // data is the last known good request result we have tracked - or if none has been tracked yet the last good result for the current args - let data = currentState.isSuccess ? currentState.data : lastResult?.data - if (data === undefined) data = currentState.data - - const hasData = data !== undefined - - // isFetching = true any time a request is in flight - const isFetching = currentState.isLoading - // isLoading = true only when loading while no data is present yet (initial load with no data in the cache) - const isLoading = !hasData && isFetching - // isSuccess = true when data is present - const isSuccess = currentState.isSuccess || (isFetching && hasData) - - return { - ...currentState, - data, - currentData: currentState.data, - isFetching, - isLoading, - isSuccess, - } as UseQueryStateDefaultResult -} - /** * Wrapper around `defaultQueryStateSelector` to be used in `useQuery`. * We want the initial render to already come back with @@ -546,6 +519,33 @@ export function buildHooks({ return { buildQueryHooks, buildMutationHook, usePrefetch } + function queryStatePreSelector( + currentState: QueryResultSelectorResult, + lastResult?: UseQueryStateDefaultResult + ): UseQueryStateDefaultResult { + // data is the last known good request result we have tracked - or if none has been tracked yet the last good result for the current args + let data = currentState.isSuccess ? currentState.data : lastResult?.data + if (data === undefined) data = currentState.data + + const hasData = data !== undefined + + // isFetching = true any time a request is in flight + const isFetching = currentState.isLoading + // isLoading = true only when loading while no data is present yet (initial load with no data in the cache) + const isLoading = !hasData && isFetching + // isSuccess = true when data is present + const isSuccess = currentState.isSuccess || (isFetching && hasData) + + return { + ...currentState, + data, + currentData: currentState.data, + isFetching, + isLoading, + isSuccess, + } as UseQueryStateDefaultResult + } + function usePrefetch>( endpointName: EndpointName, defaultOptions?: PrefetchOptions From c7d0efeb40b4cfacf3f0d3c90439b128e387b4cb Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Sat, 13 Nov 2021 14:50:01 +0100 Subject: [PATCH 2/5] fix: `api.util.resetApiState` should reset `useQuery` hooks --- .../toolkit/src/query/react/buildHooks.ts | 33 ++++++++++++- .../src/query/tests/buildHooks.test.tsx | 47 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 5a9029bb82..f65fba0167 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -521,8 +521,33 @@ export function buildHooks({ function queryStatePreSelector( currentState: QueryResultSelectorResult, - lastResult?: UseQueryStateDefaultResult + lastResult: UseQueryStateDefaultResult | undefined, + queryArgs: any ): UseQueryStateDefaultResult { + // if we had a last result and the current result is uninitialized, + // we might have called `api.util.resetApiState` + // in this case, reset the hook + if (lastResult?.endpointName && currentState.isUninitialized) { + const { endpointName } = lastResult + const endpointDefinition = context.endpointDefinitions[endpointName] + if ( + serializeQueryArgs({ + queryArgs: lastResult.originalArgs, + endpointDefinition, + endpointName, + }) === + serializeQueryArgs({ + queryArgs, + endpointDefinition, + endpointName, + }) + ) + return { + isFetching: false, + ...currentState, + } + } + // data is the last known good request result we have tracked - or if none has been tracked yet the last good result for the current args let data = currentState.isSuccess ? currentState.data : lastResult?.data if (data === undefined) data = currentState.data @@ -733,7 +758,11 @@ export function buildHooks({ const selectDefaultResult = useMemo( () => createSelector( - [select(stableArg), (_: any, lastResult: any) => lastResult], + [ + select(stableArg), + (_: any, lastResult: any) => lastResult, + () => stableArg, + ], queryStatePreSelector ), [select, stableArg] diff --git a/packages/toolkit/src/query/tests/buildHooks.test.tsx b/packages/toolkit/src/query/tests/buildHooks.test.tsx index 51b4aef085..9c2bace955 100644 --- a/packages/toolkit/src/query/tests/buildHooks.test.tsx +++ b/packages/toolkit/src/query/tests/buildHooks.test.tsx @@ -547,6 +547,53 @@ describe('hooks tests', () => { expect(screen.getByTestId('amount').textContent).toBe('2') ) }) + + describe('api.util.resetApiState resets hook', () => { + test('without `selectFromResult`', async () => { + const { result } = renderHook(() => api.endpoints.getUser.useQuery(5), { + wrapper: storeRef.wrapper, + }) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + act(() => void storeRef.store.dispatch(api.util.resetApiState())) + + expect(result.current).toEqual({ + isError: false, + isFetching: true, + isLoading: true, + isSuccess: false, + isUninitialized: false, + refetch: expect.any(Function), + status: 'pending', + }) + }) + test('with `selectFromResult`', async () => { + const { result } = renderHook( + () => + api.endpoints.getUser.useQuery(5, { + selectFromResult: (x) => x, + }), + { + wrapper: storeRef.wrapper, + } + ) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + act(() => void storeRef.store.dispatch(api.util.resetApiState())) + + expect(result.current).toEqual({ + isError: false, + isFetching: false, + isLoading: false, + isSuccess: false, + isUninitialized: true, + refetch: expect.any(Function), + status: 'uninitialized', + }) + }) + }) }) describe('useLazyQuery', () => { From 76e570ec07ec46d17c45316842bdfbcad584fdd8 Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Sun, 14 Nov 2021 20:18:39 +0100 Subject: [PATCH 3/5] trigger hook refetching after api reset --- .eslintrc.js | 6 ++ .../toolkit/src/query/core/buildInitiate.ts | 2 + .../toolkit/src/query/react/buildHooks.ts | 61 +++++++++++++------ .../src/query/tests/buildHooks.test.tsx | 31 +++++----- packages/toolkit/src/query/tests/helpers.tsx | 2 + 5 files changed, 67 insertions(+), 35 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 0184a7a466..5a71532bbb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,6 +18,12 @@ module.exports = { 'error', { prefer: 'type-imports', disallowTypeAnnotations: false }, ], + 'react-hooks/exhaustive-deps': [ + 'warn', + { + additionalHooks: '(usePossiblyImmediateEffect)', + }, + ], }, overrides: [ // { diff --git a/packages/toolkit/src/query/core/buildInitiate.ts b/packages/toolkit/src/query/core/buildInitiate.ts index 9462ddcded..717c0d2ea1 100644 --- a/packages/toolkit/src/query/core/buildInitiate.ts +++ b/packages/toolkit/src/query/core/buildInitiate.ts @@ -55,6 +55,7 @@ export type QueryActionCreatorResult< unsubscribe(): void refetch(): void updateSubscriptionOptions(options: SubscriptionOptions): void + queryCacheKey: string } type StartMutationActionCreator< @@ -284,6 +285,7 @@ Features like automatic cache collection, automatic refetching etc. will not be arg, requestId, subscriptionOptions, + queryCacheKey, abort, refetch() { dispatch( diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index f65fba0167..4325e912f0 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -620,35 +620,58 @@ export function buildHooks({ const promiseRef = useRef>() + let { queryCacheKey, requestId } = promiseRef.current || {} + const subscriptionRemoved = useSelector( + (state: RootState) => + !!queryCacheKey && + !!requestId && + !state[api.reducerPath].subscriptions[queryCacheKey]?.[requestId] + ) + usePossiblyImmediateEffect((): void | undefined => { - const lastPromise = promiseRef.current + promiseRef.current = undefined + }, [subscriptionRemoved]) - if (stableArg === skipToken) { - lastPromise?.unsubscribe() - promiseRef.current = undefined - return - } + usePossiblyImmediateEffect((): void | undefined => { + batch(() => { + const lastPromise = promiseRef.current + if ( + typeof process !== 'undefined' && + process.env.NODE_ENV === 'removeMeOnCompilation' + ) { + // this is only present to enforce the rule of hooks to keep `isSubscribed` in the dependency array + console.log(subscriptionRemoved) + } - const lastSubscriptionOptions = promiseRef.current?.subscriptionOptions + if (stableArg === skipToken) { + lastPromise?.unsubscribe() + promiseRef.current = undefined + return + } - if (!lastPromise || lastPromise.arg !== stableArg) { - lastPromise?.unsubscribe() - const promise = dispatch( - initiate(stableArg, { - subscriptionOptions: stableSubscriptionOptions, - forceRefetch: refetchOnMountOrArgChange, - }) - ) - promiseRef.current = promise - } else if (stableSubscriptionOptions !== lastSubscriptionOptions) { - lastPromise.updateSubscriptionOptions(stableSubscriptionOptions) - } + const lastSubscriptionOptions = + promiseRef.current?.subscriptionOptions + + if (!lastPromise || lastPromise.arg !== stableArg) { + lastPromise?.unsubscribe() + const promise = dispatch( + initiate(stableArg, { + subscriptionOptions: stableSubscriptionOptions, + forceRefetch: refetchOnMountOrArgChange, + }) + ) + promiseRef.current = promise + } else if (stableSubscriptionOptions !== lastSubscriptionOptions) { + lastPromise.updateSubscriptionOptions(stableSubscriptionOptions) + } + }) }, [ dispatch, initiate, refetchOnMountOrArgChange, stableArg, stableSubscriptionOptions, + subscriptionRemoved, ]) useEffect(() => { diff --git a/packages/toolkit/src/query/tests/buildHooks.test.tsx b/packages/toolkit/src/query/tests/buildHooks.test.tsx index 9c2bace955..198b15648e 100644 --- a/packages/toolkit/src/query/tests/buildHooks.test.tsx +++ b/packages/toolkit/src/query/tests/buildHooks.test.tsx @@ -558,38 +558,37 @@ describe('hooks tests', () => { act(() => void storeRef.store.dispatch(api.util.resetApiState())) - expect(result.current).toEqual({ - isError: false, - isFetching: true, - isLoading: true, - isSuccess: false, - isUninitialized: false, - refetch: expect.any(Function), - status: 'pending', - }) + expect(result.current).toEqual( + expect.objectContaining({ + isError: false, + isFetching: true, + isLoading: true, + isSuccess: false, + isUninitialized: false, + refetch: expect.any(Function), + status: 'pending', + }) + ) }) test('with `selectFromResult`', async () => { + const selectFromResult = jest.fn((x) => x) const { result } = renderHook( - () => - api.endpoints.getUser.useQuery(5, { - selectFromResult: (x) => x, - }), + () => api.endpoints.getUser.useQuery(5, { selectFromResult }), { wrapper: storeRef.wrapper, } ) await waitFor(() => expect(result.current.isSuccess).toBe(true)) - + selectFromResult.mockClear() act(() => void storeRef.store.dispatch(api.util.resetApiState())) - expect(result.current).toEqual({ + expect(selectFromResult).toHaveBeenNthCalledWith(1, { isError: false, isFetching: false, isLoading: false, isSuccess: false, isUninitialized: true, - refetch: expect.any(Function), status: 'uninitialized', }) }) diff --git a/packages/toolkit/src/query/tests/helpers.tsx b/packages/toolkit/src/query/tests/helpers.tsx index 0084822570..c42b4f79e7 100644 --- a/packages/toolkit/src/query/tests/helpers.tsx +++ b/packages/toolkit/src/query/tests/helpers.tsx @@ -17,6 +17,7 @@ import { createConsole, getLog, } from 'console-testing-library/pure' +import { cleanup } from '@testing-library/react' export const ANY = 0 as any @@ -213,6 +214,7 @@ export function setupApiStore< } }) afterEach(() => { + cleanup() if (!withoutListeners) { cleanupListeners() } From 19d725b879a96710b497ace496fd25754e463250 Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Sun, 14 Nov 2021 20:35:17 +0100 Subject: [PATCH 4/5] remove unneccessary batch --- .../toolkit/src/query/react/buildHooks.ts | 55 +++++++++---------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 4325e912f0..5dbcb0d774 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -633,38 +633,35 @@ export function buildHooks({ }, [subscriptionRemoved]) usePossiblyImmediateEffect((): void | undefined => { - batch(() => { - const lastPromise = promiseRef.current - if ( - typeof process !== 'undefined' && - process.env.NODE_ENV === 'removeMeOnCompilation' - ) { - // this is only present to enforce the rule of hooks to keep `isSubscribed` in the dependency array - console.log(subscriptionRemoved) - } + const lastPromise = promiseRef.current + if ( + typeof process !== 'undefined' && + process.env.NODE_ENV === 'removeMeOnCompilation' + ) { + // this is only present to enforce the rule of hooks to keep `isSubscribed` in the dependency array + console.log(subscriptionRemoved) + } - if (stableArg === skipToken) { - lastPromise?.unsubscribe() - promiseRef.current = undefined - return - } + if (stableArg === skipToken) { + lastPromise?.unsubscribe() + promiseRef.current = undefined + return + } - const lastSubscriptionOptions = - promiseRef.current?.subscriptionOptions + const lastSubscriptionOptions = promiseRef.current?.subscriptionOptions - if (!lastPromise || lastPromise.arg !== stableArg) { - lastPromise?.unsubscribe() - const promise = dispatch( - initiate(stableArg, { - subscriptionOptions: stableSubscriptionOptions, - forceRefetch: refetchOnMountOrArgChange, - }) - ) - promiseRef.current = promise - } else if (stableSubscriptionOptions !== lastSubscriptionOptions) { - lastPromise.updateSubscriptionOptions(stableSubscriptionOptions) - } - }) + if (!lastPromise || lastPromise.arg !== stableArg) { + lastPromise?.unsubscribe() + const promise = dispatch( + initiate(stableArg, { + subscriptionOptions: stableSubscriptionOptions, + forceRefetch: refetchOnMountOrArgChange, + }) + ) + promiseRef.current = promise + } else if (stableSubscriptionOptions !== lastSubscriptionOptions) { + lastPromise.updateSubscriptionOptions(stableSubscriptionOptions) + } }, [ dispatch, initiate, From 3d47bbed719d879a808bea62c01d44087a5eaf7c Mon Sep 17 00:00:00 2001 From: Lenz Weber Date: Fri, 26 Nov 2021 11:38:48 +0100 Subject: [PATCH 5/5] reduce size --- packages/toolkit/src/query/react/buildHooks.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 5dbcb0d774..60486c2deb 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -542,10 +542,7 @@ export function buildHooks({ endpointName, }) ) - return { - isFetching: false, - ...currentState, - } + lastResult = undefined } // data is the last known good request result we have tracked - or if none has been tracked yet the last good result for the current args