Skip to content

Commit 2e9eb2a

Browse files
authored
fix: api.util.resetApiState should reset useQuery hooks (#1735)
1 parent bee3f8a commit 2e9eb2a

File tree

5 files changed

+130
-28
lines changed

5 files changed

+130
-28
lines changed

.eslintrc.js

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ module.exports = {
1818
'error',
1919
{ prefer: 'type-imports', disallowTypeAnnotations: false },
2020
],
21+
'react-hooks/exhaustive-deps': [
22+
'warn',
23+
{
24+
additionalHooks: '(usePossiblyImmediateEffect)',
25+
},
26+
],
2127
},
2228
overrides: [
2329
// {

packages/toolkit/src/query/core/buildInitiate.ts

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export type QueryActionCreatorResult<
5757
unsubscribe(): void
5858
refetch(): void
5959
updateSubscriptionOptions(options: SubscriptionOptions): void
60+
queryCacheKey: string
6061
}
6162

6263
type StartMutationActionCreator<
@@ -286,6 +287,7 @@ Features like automatic cache collection, automatic refetching etc. will not be
286287
arg,
287288
requestId,
288289
subscriptionOptions,
290+
queryCacheKey,
289291
abort,
290292
unwrap,
291293
refetch() {

packages/toolkit/src/query/react/buildHooks.ts

+74-28
Original file line numberDiff line numberDiff line change
@@ -474,33 +474,6 @@ export type MutationTrigger<D extends MutationDefinition<any, any, any, any>> =
474474
const defaultQueryStateSelector: QueryStateSelector<any, any> = (x) => x
475475
const defaultMutationStateSelector: MutationStateSelector<any, any> = (x) => x
476476

477-
const queryStatePreSelector = (
478-
currentState: QueryResultSelectorResult<any>,
479-
lastResult: UseQueryStateDefaultResult<any>
480-
): UseQueryStateDefaultResult<any> => {
481-
// 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
482-
let data = currentState.isSuccess ? currentState.data : lastResult?.data
483-
if (data === undefined) data = currentState.data
484-
485-
const hasData = data !== undefined
486-
487-
// isFetching = true any time a request is in flight
488-
const isFetching = currentState.isLoading
489-
// isLoading = true only when loading while no data is present yet (initial load with no data in the cache)
490-
const isLoading = !hasData && isFetching
491-
// isSuccess = true when data is present
492-
const isSuccess = currentState.isSuccess || (isFetching && hasData)
493-
494-
return {
495-
...currentState,
496-
data,
497-
currentData: currentState.data,
498-
isFetching,
499-
isLoading,
500-
isSuccess,
501-
} as UseQueryStateDefaultResult<any>
502-
}
503-
504477
/**
505478
* Wrapper around `defaultQueryStateSelector` to be used in `useQuery`.
506479
* We want the initial render to already come back with
@@ -560,6 +533,55 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
560533

561534
return { buildQueryHooks, buildMutationHook, usePrefetch }
562535

536+
function queryStatePreSelector(
537+
currentState: QueryResultSelectorResult<any>,
538+
lastResult: UseQueryStateDefaultResult<any> | undefined,
539+
queryArgs: any
540+
): UseQueryStateDefaultResult<any> {
541+
// if we had a last result and the current result is uninitialized,
542+
// we might have called `api.util.resetApiState`
543+
// in this case, reset the hook
544+
if (lastResult?.endpointName && currentState.isUninitialized) {
545+
const { endpointName } = lastResult
546+
const endpointDefinition = context.endpointDefinitions[endpointName]
547+
if (
548+
serializeQueryArgs({
549+
queryArgs: lastResult.originalArgs,
550+
endpointDefinition,
551+
endpointName,
552+
}) ===
553+
serializeQueryArgs({
554+
queryArgs,
555+
endpointDefinition,
556+
endpointName,
557+
})
558+
)
559+
lastResult = undefined
560+
}
561+
562+
// 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
563+
let data = currentState.isSuccess ? currentState.data : lastResult?.data
564+
if (data === undefined) data = currentState.data
565+
566+
const hasData = data !== undefined
567+
568+
// isFetching = true any time a request is in flight
569+
const isFetching = currentState.isLoading
570+
// isLoading = true only when loading while no data is present yet (initial load with no data in the cache)
571+
const isLoading = !hasData && isFetching
572+
// isSuccess = true when data is present
573+
const isSuccess = currentState.isSuccess || (isFetching && hasData)
574+
575+
return {
576+
...currentState,
577+
data,
578+
currentData: currentState.data,
579+
isFetching,
580+
isLoading,
581+
isSuccess,
582+
} as UseQueryStateDefaultResult<any>
583+
}
584+
563585
function usePrefetch<EndpointName extends QueryKeys<Definitions>>(
564586
endpointName: EndpointName,
565587
defaultOptions?: PrefetchOptions
@@ -609,8 +631,27 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
609631

610632
const promiseRef = useRef<QueryActionCreatorResult<any>>()
611633

634+
let { queryCacheKey, requestId } = promiseRef.current || {}
635+
const subscriptionRemoved = useSelector(
636+
(state: RootState<Definitions, string, string>) =>
637+
!!queryCacheKey &&
638+
!!requestId &&
639+
!state[api.reducerPath].subscriptions[queryCacheKey]?.[requestId]
640+
)
641+
642+
usePossiblyImmediateEffect((): void | undefined => {
643+
promiseRef.current = undefined
644+
}, [subscriptionRemoved])
645+
612646
usePossiblyImmediateEffect((): void | undefined => {
613647
const lastPromise = promiseRef.current
648+
if (
649+
typeof process !== 'undefined' &&
650+
process.env.NODE_ENV === 'removeMeOnCompilation'
651+
) {
652+
// this is only present to enforce the rule of hooks to keep `isSubscribed` in the dependency array
653+
console.log(subscriptionRemoved)
654+
}
614655

615656
if (stableArg === skipToken) {
616657
lastPromise?.unsubscribe()
@@ -638,6 +679,7 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
638679
refetchOnMountOrArgChange,
639680
stableArg,
640681
stableSubscriptionOptions,
682+
subscriptionRemoved,
641683
])
642684

643685
useEffect(() => {
@@ -752,7 +794,11 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
752794
const selectDefaultResult = useMemo(
753795
() =>
754796
createSelector(
755-
[select(stableArg), (_: any, lastResult: any) => lastResult],
797+
[
798+
select(stableArg),
799+
(_: any, lastResult: any) => lastResult,
800+
() => stableArg,
801+
],
756802
queryStatePreSelector
757803
),
758804
[select, stableArg]

packages/toolkit/src/query/tests/buildHooks.test.tsx

+46
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,52 @@ describe('hooks tests', () => {
550550
expect(screen.getByTestId('amount').textContent).toBe('2')
551551
)
552552
})
553+
554+
describe('api.util.resetApiState resets hook', () => {
555+
test('without `selectFromResult`', async () => {
556+
const { result } = renderHook(() => api.endpoints.getUser.useQuery(5), {
557+
wrapper: storeRef.wrapper,
558+
})
559+
560+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
561+
562+
act(() => void storeRef.store.dispatch(api.util.resetApiState()))
563+
564+
expect(result.current).toEqual(
565+
expect.objectContaining({
566+
isError: false,
567+
isFetching: true,
568+
isLoading: true,
569+
isSuccess: false,
570+
isUninitialized: false,
571+
refetch: expect.any(Function),
572+
status: 'pending',
573+
})
574+
)
575+
})
576+
test('with `selectFromResult`', async () => {
577+
const selectFromResult = jest.fn((x) => x)
578+
const { result } = renderHook(
579+
() => api.endpoints.getUser.useQuery(5, { selectFromResult }),
580+
{
581+
wrapper: storeRef.wrapper,
582+
}
583+
)
584+
585+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
586+
selectFromResult.mockClear()
587+
act(() => void storeRef.store.dispatch(api.util.resetApiState()))
588+
589+
expect(selectFromResult).toHaveBeenNthCalledWith(1, {
590+
isError: false,
591+
isFetching: false,
592+
isLoading: false,
593+
isSuccess: false,
594+
isUninitialized: true,
595+
status: 'uninitialized',
596+
})
597+
})
598+
})
553599
})
554600

555601
describe('useLazyQuery', () => {

packages/toolkit/src/query/tests/helpers.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
createConsole,
1818
getLog,
1919
} from 'console-testing-library/pure'
20+
import { cleanup } from '@testing-library/react'
2021

2122
export const ANY = 0 as any
2223

@@ -213,6 +214,7 @@ export function setupApiStore<
213214
}
214215
})
215216
afterEach(() => {
217+
cleanup()
216218
if (!withoutListeners) {
217219
cleanupListeners()
218220
}

0 commit comments

Comments
 (0)