Skip to content

Commit 7eb076a

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

File tree

2 files changed

+101
-4
lines changed

2 files changed

+101
-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: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ import {
1010
QueryStatus,
1111
skipToken,
1212
} from '@reduxjs/toolkit/query/react'
13-
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
13+
import {
14+
act,
15+
fireEvent,
16+
render,
17+
screen,
18+
waitFor,
19+
renderHook,
20+
} from '@testing-library/react'
1421
import userEvent from '@testing-library/user-event'
1522
import { rest } from 'msw'
1623
import {
@@ -26,10 +33,10 @@ import {
2633
import { server } from './mocks/server'
2734
import type { AnyAction } from 'redux'
2835
import type { SubscriptionOptions } from '@reduxjs/toolkit/dist/query/core/apiState'
29-
import type { SerializedError } from '@reduxjs/toolkit'
36+
import { nanoid, SerializedError } from '@reduxjs/toolkit'
3037
import { createListenerMiddleware, configureStore } from '@reduxjs/toolkit'
31-
import { renderHook } from '@testing-library/react'
3238
import { delay } from '../../utils'
39+
import { useSelector } from 'react-redux'
3340

3441
// Just setup a temporary in-memory counter for tests that `getIncrementedAmount`.
3542
// This can be used to test how many renders happen due to data changes or
@@ -715,6 +722,94 @@ describe('hooks tests', () => {
715722
expect(res.data!.amount).toBeGreaterThan(originalAmount)
716723
})
717724

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

0 commit comments

Comments
 (0)