| 1 | // tests for "cleanup-after-unsubscribe" behavior
|
|---|
| 2 | import React from 'react'
|
|---|
| 3 |
|
|---|
| 4 | import { createListenerMiddleware } from '@reduxjs/toolkit'
|
|---|
| 5 | import { createApi, QueryStatus } from '@reduxjs/toolkit/query/react'
|
|---|
| 6 | import { act, render, screen, waitFor } from '@testing-library/react'
|
|---|
| 7 | import { setupApiStore } from '../../tests/utils/helpers'
|
|---|
| 8 | import type { SubscriptionSelectors } from '../core/buildMiddleware/types'
|
|---|
| 9 |
|
|---|
| 10 | const api = createApi({
|
|---|
| 11 | baseQuery: () => ({ data: 42 }),
|
|---|
| 12 | endpoints: (build) => ({
|
|---|
| 13 | a: build.query<unknown, void>({ query: () => '' }),
|
|---|
| 14 | b: build.query<unknown, void>({ query: () => '' }),
|
|---|
| 15 | }),
|
|---|
| 16 | })
|
|---|
| 17 | const storeRef = setupApiStore(api)
|
|---|
| 18 |
|
|---|
| 19 | const getSubStateA = () => storeRef.store.getState().api.queries['a(undefined)']
|
|---|
| 20 | const getSubStateB = () => storeRef.store.getState().api.queries['b(undefined)']
|
|---|
| 21 |
|
|---|
| 22 | function UsingA() {
|
|---|
| 23 | const { data } = api.endpoints.a.useQuery()
|
|---|
| 24 |
|
|---|
| 25 | return <>Result: {data as React.ReactNode} </>
|
|---|
| 26 | }
|
|---|
| 27 |
|
|---|
| 28 | function UsingB() {
|
|---|
| 29 | api.endpoints.b.useQuery()
|
|---|
| 30 |
|
|---|
| 31 | return <></>
|
|---|
| 32 | }
|
|---|
| 33 |
|
|---|
| 34 | function UsingAB() {
|
|---|
| 35 | api.endpoints.a.useQuery()
|
|---|
| 36 | api.endpoints.b.useQuery()
|
|---|
| 37 |
|
|---|
| 38 | return <></>
|
|---|
| 39 | }
|
|---|
| 40 |
|
|---|
| 41 | beforeAll(() => {
|
|---|
| 42 | vi.useFakeTimers({ shouldAdvanceTime: true })
|
|---|
| 43 | })
|
|---|
| 44 |
|
|---|
| 45 | test('data stays in store when component stays rendered', async () => {
|
|---|
| 46 | expect(getSubStateA()).toBeUndefined()
|
|---|
| 47 |
|
|---|
| 48 | render(<UsingA />, { wrapper: storeRef.wrapper })
|
|---|
| 49 | await waitFor(() =>
|
|---|
| 50 | expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled),
|
|---|
| 51 | )
|
|---|
| 52 |
|
|---|
| 53 | vi.advanceTimersByTime(120_000)
|
|---|
| 54 |
|
|---|
| 55 | expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled)
|
|---|
| 56 | })
|
|---|
| 57 |
|
|---|
| 58 | test('data is removed from store after 60 seconds', async () => {
|
|---|
| 59 | expect(getSubStateA()).toBeUndefined()
|
|---|
| 60 |
|
|---|
| 61 | const { unmount } = render(<UsingA />, { wrapper: storeRef.wrapper })
|
|---|
| 62 | await waitFor(() =>
|
|---|
| 63 | expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled),
|
|---|
| 64 | )
|
|---|
| 65 |
|
|---|
| 66 | unmount()
|
|---|
| 67 |
|
|---|
| 68 | vi.advanceTimersByTime(59_000)
|
|---|
| 69 |
|
|---|
| 70 | expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled)
|
|---|
| 71 |
|
|---|
| 72 | vi.advanceTimersByTime(2000)
|
|---|
| 73 |
|
|---|
| 74 | expect(getSubStateA()).toBeUndefined()
|
|---|
| 75 | })
|
|---|
| 76 |
|
|---|
| 77 | test('data stays in store when component stays rendered while data for another component is removed after it unmounted', async () => {
|
|---|
| 78 | expect(getSubStateA()).toBeUndefined()
|
|---|
| 79 | expect(getSubStateB()).toBeUndefined()
|
|---|
| 80 |
|
|---|
| 81 | const { rerender } = render(
|
|---|
| 82 | <>
|
|---|
| 83 | <UsingA />
|
|---|
| 84 | <UsingB />
|
|---|
| 85 | </>,
|
|---|
| 86 | { wrapper: storeRef.wrapper },
|
|---|
| 87 | )
|
|---|
| 88 | await waitFor(() => {
|
|---|
| 89 | expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled)
|
|---|
| 90 | expect(getSubStateB()?.status).toBe(QueryStatus.fulfilled)
|
|---|
| 91 | })
|
|---|
| 92 |
|
|---|
| 93 | const statusA = getSubStateA()
|
|---|
| 94 |
|
|---|
| 95 | await act(async () => {
|
|---|
| 96 | rerender(<UsingA />)
|
|---|
| 97 |
|
|---|
| 98 | vi.advanceTimersByTime(10)
|
|---|
| 99 | })
|
|---|
| 100 |
|
|---|
| 101 | vi.advanceTimersByTime(120_000)
|
|---|
| 102 |
|
|---|
| 103 | expect(getSubStateA()).toEqual(statusA)
|
|---|
| 104 | expect(getSubStateB()).toBeUndefined()
|
|---|
| 105 | })
|
|---|
| 106 |
|
|---|
| 107 | test('data stays in store when one component requiring the data stays in the store', async () => {
|
|---|
| 108 | expect(getSubStateA()).toBeUndefined()
|
|---|
| 109 | expect(getSubStateB()).toBeUndefined()
|
|---|
| 110 |
|
|---|
| 111 | const { rerender } = render(
|
|---|
| 112 | <>
|
|---|
| 113 | <UsingA key="a" />
|
|---|
| 114 | <UsingAB key="ab" />
|
|---|
| 115 | </>,
|
|---|
| 116 | { wrapper: storeRef.wrapper },
|
|---|
| 117 | )
|
|---|
| 118 | await waitFor(() => {
|
|---|
| 119 | expect(getSubStateA()?.status).toBe(QueryStatus.fulfilled)
|
|---|
| 120 | expect(getSubStateB()?.status).toBe(QueryStatus.fulfilled)
|
|---|
| 121 | })
|
|---|
| 122 |
|
|---|
| 123 | const statusA = getSubStateA()
|
|---|
| 124 | const statusB = getSubStateB()
|
|---|
| 125 |
|
|---|
| 126 | await act(async () => {
|
|---|
| 127 | rerender(<UsingAB key="ab" />)
|
|---|
| 128 | vi.advanceTimersByTime(10)
|
|---|
| 129 | vi.runAllTimers()
|
|---|
| 130 | })
|
|---|
| 131 |
|
|---|
| 132 | await act(async () => {
|
|---|
| 133 | vi.advanceTimersByTime(120000)
|
|---|
| 134 | vi.runAllTimers()
|
|---|
| 135 | })
|
|---|
| 136 |
|
|---|
| 137 | expect(getSubStateA()).toEqual(statusA)
|
|---|
| 138 | expect(getSubStateB()).toEqual(statusB)
|
|---|
| 139 | })
|
|---|
| 140 |
|
|---|
| 141 | test('Minimizes the number of subscription dispatches when multiple components ask for the same data', async () => {
|
|---|
| 142 | const listenerMiddleware = createListenerMiddleware()
|
|---|
| 143 | const storeRef = setupApiStore(api, undefined, {
|
|---|
| 144 | middleware: {
|
|---|
| 145 | concat: [listenerMiddleware.middleware],
|
|---|
| 146 | },
|
|---|
| 147 | withoutTestLifecycles: true,
|
|---|
| 148 | })
|
|---|
| 149 |
|
|---|
| 150 | const actionTypes: unknown[] = []
|
|---|
| 151 |
|
|---|
| 152 | listenerMiddleware.startListening({
|
|---|
| 153 | predicate: () => true,
|
|---|
| 154 | effect: (action) => {
|
|---|
| 155 | if (
|
|---|
| 156 | action.type.includes('subscriptionsUpdated') ||
|
|---|
| 157 | action.type.includes('internal_')
|
|---|
| 158 | ) {
|
|---|
| 159 | return
|
|---|
| 160 | }
|
|---|
| 161 |
|
|---|
| 162 | actionTypes.push(action.type)
|
|---|
| 163 | },
|
|---|
| 164 | })
|
|---|
| 165 |
|
|---|
| 166 | const { getSubscriptionCount } = storeRef.store.dispatch(
|
|---|
| 167 | api.internalActions.internal_getRTKQSubscriptions(),
|
|---|
| 168 | ) as unknown as SubscriptionSelectors
|
|---|
| 169 |
|
|---|
| 170 | const NUM_LIST_ITEMS = 1000
|
|---|
| 171 |
|
|---|
| 172 | function ParentComponent() {
|
|---|
| 173 | const listItems = Array.from({ length: NUM_LIST_ITEMS }).map((_, i) => (
|
|---|
| 174 | <UsingA key={i} />
|
|---|
| 175 | ))
|
|---|
| 176 |
|
|---|
| 177 | return <>{listItems}</>
|
|---|
| 178 | }
|
|---|
| 179 |
|
|---|
| 180 | render(<ParentComponent />, {
|
|---|
| 181 | wrapper: storeRef.wrapper,
|
|---|
| 182 | })
|
|---|
| 183 |
|
|---|
| 184 | await act(async () => {
|
|---|
| 185 | vi.advanceTimersByTime(10)
|
|---|
| 186 | vi.runAllTimers()
|
|---|
| 187 | })
|
|---|
| 188 |
|
|---|
| 189 | await waitFor(() => {
|
|---|
| 190 | return screen.getAllByText(/42/).length > 0
|
|---|
| 191 | })
|
|---|
| 192 |
|
|---|
| 193 | expect(getSubscriptionCount('a(undefined)')).toBe(NUM_LIST_ITEMS)
|
|---|
| 194 |
|
|---|
| 195 | expect(actionTypes).toEqual([
|
|---|
| 196 | 'api/config/middlewareRegistered',
|
|---|
| 197 | 'api/executeQuery/pending',
|
|---|
| 198 | 'api/executeQuery/fulfilled',
|
|---|
| 199 | ])
|
|---|
| 200 | }, 25_000)
|
|---|