Skip to content
This repository was archived by the owner on Nov 23, 2024. It is now read-only.

Commit 34db229

Browse files
committed
Unsubscribe existing query when arg changes on a lazy query
1 parent b097568 commit 34db229

File tree

5 files changed

+97
-7
lines changed

5 files changed

+97
-7
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@
110110
],
111111
"dependencies": {
112112
"@babel/runtime": "^7.12.5",
113-
"immer": ">=8.0.0"
113+
"immer": ">=8.0.0",
114+
"react-fast-compare": "^3.2.0"
114115
},
115116
"peerDependencies": {
116117
"@reduxjs/toolkit": "^1.5.0",

src/react-hooks/buildHooks.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { Id, NoInfer, Override } from '../tsHelpers';
2424
import { ApiEndpointMutation, ApiEndpointQuery, CoreModule, PrefetchOptions } from '../core/module';
2525
import { ReactHooksModuleOptions } from './module';
2626
import { useShallowStableValue } from './useShallowStableValue';
27+
import isDeepEqual from 'react-fast-compare';
2728

2829
export interface QueryHooks<Definition extends QueryDefinition<any, any, any, any, any>> {
2930
useQuery: UseQuery<Definition>;
@@ -253,6 +254,7 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
253254
const lastPromise = promiseRef.current;
254255

255256
const optionsRef = useRef<SubscriptionOptions>();
257+
const argsRef = useRef<any>();
256258

257259
useEffect(() => {
258260
const options = {
@@ -274,23 +276,36 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
274276
promiseRef.current?.unsubscribe();
275277
promiseRef.current = undefined;
276278
optionsRef.current = undefined;
279+
argsRef.current = undefined;
277280
};
278281
}, []);
279282

280283
const trigger = useCallback(
281284
function (args: any) {
285+
if (!argsRef.current) {
286+
argsRef.current = args;
287+
} else if (argsRef.current) {
288+
// args have changed, we need to unsubscribe before creating the new subscription ref.
289+
if (!isDeepEqual(argsRef.current, args)) {
290+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
291+
lastPromise?.unsubscribe();
292+
argsRef.current = args;
293+
}
294+
}
295+
296+
// Set the subscription options on the initial query
297+
if (!optionsRef.current) {
298+
optionsRef.current = { pollingInterval, refetchOnReconnect, refetchOnFocus };
299+
}
300+
282301
promiseRef.current = dispatch(
283302
initiate(args, {
284303
subscriptionOptions: { pollingInterval, refetchOnReconnect, refetchOnFocus },
285304
forceRefetch: true,
286305
})
287306
);
288-
// Set the subscription options on the initial query
289-
if (!optionsRef.current) {
290-
optionsRef.current = { pollingInterval, refetchOnReconnect, refetchOnFocus };
291-
}
292307
},
293-
[dispatch, initiate, pollingInterval, refetchOnFocus, refetchOnReconnect]
308+
[dispatch, initiate, lastPromise, pollingInterval, refetchOnFocus, refetchOnReconnect]
294309
);
295310

296311
return [trigger, promiseRef];

test/buildHooks.test.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,65 @@ describe('hooks tests', () => {
466466
storeRef.store.getState().actions.filter(api.internalActions.updateSubscriptionOptions.match)
467467
).toHaveLength(1);
468468
});
469+
470+
test('useLazyQuery accepts updated args and unsubscribes the original query', async () => {
471+
function User() {
472+
const [fetchUser, { data: hookData, isFetching, isUninitialized }] = api.endpoints.getUser.useLazyQuery();
473+
474+
data = hookData;
475+
476+
return (
477+
<div>
478+
<div data-testid="isUninitialized">{String(isUninitialized)}</div>
479+
<div data-testid="isFetching">{String(isFetching)}</div>
480+
481+
<button data-testid="fetchUser1" onClick={() => fetchUser(1)}>
482+
fetchUser1
483+
</button>
484+
<button data-testid="fetchUser2" onClick={() => fetchUser(2)}>
485+
fetchUser2
486+
</button>
487+
</div>
488+
);
489+
}
490+
491+
render(<User />, { wrapper: storeRef.wrapper });
492+
493+
await waitFor(() => expect(screen.getByTestId('isUninitialized').textContent).toBe('true'));
494+
await waitFor(() => expect(data).toBeUndefined());
495+
496+
fireEvent.click(screen.getByTestId('fetchUser1'));
497+
498+
await waitFor(() => expect(screen.getByTestId('isFetching').textContent).toBe('true'));
499+
await waitFor(() => expect(screen.getByTestId('isFetching').textContent).toBe('false'));
500+
501+
// Being that there is only the initial query, no unsubscribe should be dispatched
502+
expect(storeRef.store.getState().actions.filter(api.internalActions.unsubscribeQueryResult.match)).toHaveLength(
503+
0
504+
);
505+
506+
fireEvent.click(screen.getByTestId('fetchUser2'));
507+
508+
await waitFor(() => expect(screen.getByTestId('isFetching').textContent).toBe('true'));
509+
await waitFor(() => expect(screen.getByTestId('isFetching').textContent).toBe('false'));
510+
511+
// `args` changed from 1 -> 2, should unsubscribe the original for 1.
512+
expect(storeRef.store.getState().actions.filter(api.internalActions.unsubscribeQueryResult.match)).toHaveLength(
513+
1
514+
);
515+
516+
fireEvent.click(screen.getByTestId('fetchUser1'));
517+
518+
expect(storeRef.store.getState().actions.filter(api.internalActions.unsubscribeQueryResult.match)).toHaveLength(
519+
2
520+
);
521+
522+
// no `arg` change, no unsubscribe happens, just another pending request/fulfilled
523+
fireEvent.click(screen.getByTestId('fetchUser1'));
524+
expect(storeRef.store.getState().actions.filter(api.internalActions.unsubscribeQueryResult.match)).toHaveLength(
525+
2
526+
);
527+
});
469528
});
470529

471530
describe('useMutation', () => {

test/createApi.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,10 @@ describe('additional transformResponse behaviors', () => {
439439
echo: build.mutation({
440440
query: () => ({ method: 'PUT', url: '/echo' }),
441441
}),
442+
mutation: build.mutation({
443+
query: () => ({ url: '/echo', method: 'POST', body: { nested: { banana: 'bread' } } }),
444+
transformResponse: (response: { body: { nested: EchoResponseData } }) => response.body.nested,
445+
}),
442446
query: build.query<SuccessResponse & EchoResponseData, void>({
443447
query: () => '/success',
444448
transformResponse: async (response: SuccessResponse) => {
@@ -455,11 +459,17 @@ describe('additional transformResponse behaviors', () => {
455459

456460
const storeRef = setupApiStore(api);
457461

458-
test('transformResponse handles an async transformation and returns the merged data', async () => {
462+
test('transformResponse handles an async transformation and returns the merged data (query)', async () => {
459463
const result = await storeRef.store.dispatch(api.endpoints.query.initiate());
460464

461465
expect(result.data).toEqual({ value: 'success', banana: 'bread' });
462466
});
467+
468+
test('transformResponse transforms a response from a mutation', async () => {
469+
const result = await storeRef.store.dispatch(api.endpoints.mutation.initiate({}));
470+
471+
expect(result.data).toEqual({ banana: 'bread' });
472+
});
463473
});
464474

465475
describe('query endpoint lifecycles - onStart, onSuccess, onError', () => {

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7429,6 +7429,11 @@ [email protected]:
74297429
object-assign "^4.1.1"
74307430
scheduler "^0.20.0"
74317431

7432+
react-fast-compare@^3.2.0:
7433+
version "3.2.0"
7434+
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
7435+
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
7436+
74327437
"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1:
74337438
version "17.0.1"
74347439
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"

0 commit comments

Comments
 (0)