Skip to content

Commit 364ff51

Browse files
committed
Only remove promise in query hook if the subscription was removed
1 parent 140ca1a commit 364ff51

File tree

2 files changed

+99
-4
lines changed

2 files changed

+99
-4
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,9 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
740740
})
741741

742742
usePossiblyImmediateEffect((): void | undefined => {
743-
promiseRef.current = undefined
743+
if (subscriptionRemoved) {
744+
promiseRef.current = undefined
745+
}
744746
}, [subscriptionRemoved])
745747

746748
usePossiblyImmediateEffect((): void | undefined => {

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

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import util from 'util'
21
import * as React from 'react'
32
import type {
43
UseMutation,
@@ -10,7 +9,14 @@ import {
109
QueryStatus,
1110
skipToken,
1211
} from '@reduxjs/toolkit/query/react'
13-
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
12+
import {
13+
act,
14+
fireEvent,
15+
render,
16+
screen,
17+
waitFor,
18+
renderHook,
19+
} from '@testing-library/react'
1420
import userEvent from '@testing-library/user-event'
1521
import { rest } from 'msw'
1622
import {
@@ -28,7 +34,6 @@ import type { AnyAction } from 'redux'
2834
import type { SubscriptionOptions } from '@reduxjs/toolkit/dist/query/core/apiState'
2935
import type { SerializedError } from '@reduxjs/toolkit'
3036
import { createListenerMiddleware, configureStore } from '@reduxjs/toolkit'
31-
import { renderHook } from '@testing-library/react'
3237
import { delay } from '../../utils'
3338

3439
// Just setup a temporary in-memory counter for tests that `getIncrementedAmount`.
@@ -715,6 +720,94 @@ describe('hooks tests', () => {
715720
expect(res.data!.amount).toBeGreaterThan(originalAmount)
716721
})
717722

723+
// See https://github.com/reduxjs/redux-toolkit/issues/3182
724+
test('Hook subscriptions are properly cleaned up when changing skip back and forth', async () => {
725+
const pokemonApi = createApi({
726+
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
727+
endpoints: (builder) => ({
728+
getPokemonByName: builder.query({
729+
queryFn: (name: string) => ({ data: null }),
730+
keepUnusedDataFor: 1,
731+
}),
732+
}),
733+
})
734+
735+
const storeRef = setupApiStore(pokemonApi, undefined, {
736+
withoutTestLifecycles: true,
737+
})
738+
739+
const getSubscriptions = () => storeRef.store.getState().api.subscriptions
740+
741+
const checkNumSubscriptions = (arg: string, count: number) => {
742+
const subscriptions = getSubscriptions()
743+
const cacheKeyEntry = subscriptions[arg]
744+
745+
if (cacheKeyEntry) {
746+
expect(Object.values(cacheKeyEntry).length).toBe(count)
747+
}
748+
}
749+
750+
// 1) Initial state: an active subscription
751+
const { result, rerender, unmount } = renderHook(
752+
([arg, options]: Parameters<
753+
typeof pokemonApi.useGetPokemonByNameQuery
754+
>) => pokemonApi.useGetPokemonByNameQuery(arg, options),
755+
{
756+
wrapper: storeRef.wrapper,
757+
initialProps: ['a'],
758+
}
759+
)
760+
761+
await act(async () => {
762+
await delay(1)
763+
})
764+
765+
// 2) Set the current subscription to `{skip: true}
766+
await act(async () => {
767+
rerender(['a', { skip: true }])
768+
})
769+
770+
// 3) Change _both_ the cache key _and_ `{skip: false}` at the same time.
771+
// This causes the `subscriptionRemoved` check to be `true`.
772+
await act(async () => {
773+
rerender(['b'])
774+
})
775+
776+
// There should only be one active subscription after changing the arg
777+
checkNumSubscriptions('b', 1)
778+
779+
// 4) Re-render with the same arg.
780+
// This causes the `subscriptionRemoved` check to be `false`.
781+
// Correct behavior is this does _not_ clear the promise ref,
782+
// so
783+
await act(async () => {
784+
rerender(['b'])
785+
})
786+
787+
// There should only be one active subscription after changing the arg
788+
checkNumSubscriptions('b', 1)
789+
790+
await act(async () => {
791+
await delay(1)
792+
})
793+
794+
unmount()
795+
796+
await act(async () => {
797+
await delay(1)
798+
})
799+
800+
// There should be no subscription entries left over after changing
801+
// cache key args and swapping `skip` on and off
802+
checkNumSubscriptions('b', 0)
803+
804+
const finalSubscriptions = getSubscriptions()
805+
806+
for (let cacheKeyEntry of Object.values(finalSubscriptions)) {
807+
expect(Object.values(cacheKeyEntry!).length).toBe(0)
808+
}
809+
})
810+
718811
describe('Hook middleware requirements', () => {
719812
let mock: jest.SpyInstance
720813

0 commit comments

Comments
 (0)