@@ -10,7 +10,14 @@ import {
10
10
QueryStatus ,
11
11
skipToken ,
12
12
} 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'
14
21
import userEvent from '@testing-library/user-event'
15
22
import { rest } from 'msw'
16
23
import {
@@ -26,10 +33,10 @@ import {
26
33
import { server } from './mocks/server'
27
34
import type { AnyAction } from 'redux'
28
35
import type { SubscriptionOptions } from '@reduxjs/toolkit/dist/query/core/apiState'
29
- import type { SerializedError } from '@reduxjs/toolkit'
36
+ import { nanoid , SerializedError } from '@reduxjs/toolkit'
30
37
import { createListenerMiddleware , configureStore } from '@reduxjs/toolkit'
31
- import { renderHook } from '@testing-library/react'
32
38
import { delay } from '../../utils'
39
+ import { useSelector } from 'react-redux'
33
40
34
41
// Just setup a temporary in-memory counter for tests that `getIncrementedAmount`.
35
42
// This can be used to test how many renders happen due to data changes or
@@ -715,6 +722,94 @@ describe('hooks tests', () => {
715
722
expect ( res . data ! . amount ) . toBeGreaterThan ( originalAmount )
716
723
} )
717
724
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
+
718
813
describe ( 'Hook middleware requirements' , ( ) => {
719
814
let mock : jest . SpyInstance
720
815
0 commit comments