diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index daec42efe2..c5c0a08c11 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -51,6 +51,7 @@ import { UNINITIALIZED_VALUE } from './constants' import type { ReactHooksModuleOptions } from './module' import { useStableQueryArgs } from './useSerializedStableValue' import { useShallowStableValue } from './useShallowStableValue' +import { useIsMounted } from './useIsMounted' // Copy-pasted from React-Redux const canUseDOM = () => @@ -1118,12 +1119,19 @@ export function buildHooks({ } }, []) + const getIsMounted = useIsMounted() + return useMemo( () => ({ /** * A method to manually refetch data for the query */ refetch: () => { + // If `refetch` gets called after the component is unmounted, + // this should be a no-op + if (!getIsMounted()) { + return + } if (!promiseRef.current) throw new Error( 'Cannot refetch a query that has not been started yet.', @@ -1131,7 +1139,7 @@ export function buildHooks({ return promiseRef.current?.refetch() }, }), - [], + [getIsMounted], ) } diff --git a/packages/toolkit/src/query/react/useIsMounted.ts b/packages/toolkit/src/query/react/useIsMounted.ts new file mode 100644 index 0000000000..09f8e4faa4 --- /dev/null +++ b/packages/toolkit/src/query/react/useIsMounted.ts @@ -0,0 +1,16 @@ +import { useCallback, useLayoutEffect, useRef } from 'react' + +export function useIsMounted(): () => boolean { + const mountedRef = useRef(false) + const get = useCallback(() => mountedRef.current, []) + + useLayoutEffect(() => { + mountedRef.current = true + + return () => { + mountedRef.current = false + } + }, []) + + return get +} diff --git a/packages/toolkit/src/query/tests/buildHooks.test.tsx b/packages/toolkit/src/query/tests/buildHooks.test.tsx index 6810e75dd8..8b2d7ddeef 100644 --- a/packages/toolkit/src/query/tests/buildHooks.test.tsx +++ b/packages/toolkit/src/query/tests/buildHooks.test.tsx @@ -812,6 +812,28 @@ describe('hooks tests', () => { await screen.findByText('ID: 3') }) + test('refetch does not throw an error if run after unmount', async () => { + let refetchFunction: () => {} + + function User() { + const { refetch } = api.endpoints.getUser.useQuery(1) + + refetchFunction = refetch + + return ( +
+ +
+ ) + } + + const { unmount } = render(, { wrapper: storeRef.wrapper }) + + unmount() + + expect(() => refetchFunction()).not.toThrow() + }) + test(`useQuery shouldn't call args serialization if request skipped`, async () => { expect(() => renderHook(() => api.endpoints.queryWithDeepArg.useQuery(skipToken), {