Skip to content

Commit 42f0d95

Browse files
authored
feat(shared): Support for keepPreviousData in useSubscription.rq (#7203)
1 parent c63cc8e commit 42f0d95

File tree

5 files changed

+95
-4
lines changed

5 files changed

+95
-4
lines changed

.changeset/soft-beers-sit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/shared': patch
3+
---
4+
5+
Support `keepPreviousData` behaviour in the internal React Query variant of `useSubscription`.

packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { renderHook, waitFor } from '@testing-library/react';
22
import { beforeEach, describe, expect, it, vi } from 'vitest';
33

4+
import { createDeferredPromise } from '../../../utils/createDeferredPromise';
45
import { useSubscription } from '../useSubscription';
56
import { createMockClerk, createMockOrganization, createMockQueryClient, createMockUser } from './mocks/clerk';
67
import { wrapper } from './wrapper';
@@ -144,4 +145,72 @@ describe('useSubscription', () => {
144145
expect(getSubscriptionSpy).toHaveBeenCalledTimes(1);
145146
expect(result.current.isFetching).toBe(false);
146147
});
148+
149+
it('retains previous data while refetching when keepPreviousData=true', async () => {
150+
const { result, rerender } = renderHook(
151+
({ orgId, keepPreviousData }) => {
152+
mockOrganization = createMockOrganization({ id: orgId });
153+
return useSubscription({ for: 'organization', keepPreviousData });
154+
},
155+
{
156+
wrapper,
157+
initialProps: { orgId: 'org_1', keepPreviousData: true },
158+
},
159+
);
160+
161+
await waitFor(() => expect(result.current.isLoading).toBe(false));
162+
expect(result.current.data).toEqual({ id: 'sub_org_org_1' });
163+
164+
const deferred = createDeferredPromise();
165+
getSubscriptionSpy.mockImplementationOnce(() => deferred.promise as Promise<{ id: string }>);
166+
167+
rerender({ orgId: 'org_2', keepPreviousData: true });
168+
169+
await waitFor(() => expect(result.current.isFetching).toBe(true));
170+
171+
// Slight difference in behavior between SWR and React Query, but acceptable for the migration.
172+
if (__CLERK_USE_RQ__) {
173+
await waitFor(() => expect(result.current.isLoading).toBe(false));
174+
} else {
175+
await waitFor(() => expect(result.current.isLoading).toBe(true));
176+
}
177+
expect(result.current.data).toEqual({ id: 'sub_org_org_1' });
178+
179+
deferred.resolve({ id: 'sub_org_org_2' });
180+
181+
await waitFor(() => expect(result.current.data).toEqual({ id: 'sub_org_org_2' }));
182+
expect(getSubscriptionSpy).toHaveBeenCalledTimes(2);
183+
});
184+
185+
it('clears data while refetching when keepPreviousData=false', async () => {
186+
const { result, rerender } = renderHook(
187+
({ orgId, keepPreviousData }) => {
188+
mockOrganization = createMockOrganization({ id: orgId });
189+
return useSubscription({ for: 'organization', keepPreviousData });
190+
},
191+
{
192+
wrapper,
193+
initialProps: { orgId: 'org_1', keepPreviousData: false },
194+
},
195+
);
196+
197+
await waitFor(() => expect(result.current.isLoading).toBe(false));
198+
expect(result.current.data).toEqual({ id: 'sub_org_org_1' });
199+
200+
const deferred = createDeferredPromise();
201+
getSubscriptionSpy.mockImplementationOnce(() => deferred.promise as Promise<{ id: string }>);
202+
203+
rerender({ orgId: 'org_2', keepPreviousData: false });
204+
205+
await waitFor(() => expect(result.current.isFetching).toBe(true));
206+
expect(result.current.isLoading).toBe(true);
207+
expect(result.current.data).toBeUndefined();
208+
209+
deferred.resolve({ id: 'sub_org_org_2' });
210+
211+
await waitFor(() => expect(result.current.isFetching).toBe(false));
212+
expect(result.current.data).toEqual({ id: 'sub_org_org_2' });
213+
expect(result.current.isLoading).toBe(false);
214+
expect(getSubscriptionSpy).toHaveBeenCalledTimes(2);
215+
});
147216
});

packages/shared/src/react/hooks/createBillingPaginatedHook.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,10 @@ export function createBillingPaginatedHook<TResource extends ClerkResource, TPar
105105
(hookParams || {}) as TParams,
106106
fetchFn,
107107
{
108-
keepPreviousData: safeValues.keepPreviousData,
109-
infinite: safeValues.infinite,
108+
...({
109+
keepPreviousData: safeValues.keepPreviousData,
110+
infinite: safeValues.infinite,
111+
} as PaginatedHookConfig<unknown>),
110112
enabled: isEnabled,
111113
...(options?.unauthenticated ? {} : { isSignedIn: Boolean(user) }),
112114
__experimental_mode: safeValues.__experimental_mode,

packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ import type { UsePagesOrInfiniteSignature } from './usePageOrInfinite.types';
1111
import { getDifferentKeys, useWithSafeValues } from './usePagesOrInfinite.shared';
1212
import { usePreviousValue } from './usePreviousValue';
1313

14+
/**
15+
* @internal
16+
*/
17+
function KeepPreviousDataFn<Data>(previousData: Data): Data {
18+
return previousData;
19+
}
20+
1421
export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher, config, cacheKeys) => {
1522
const [paginatedPage, setPaginatedPage] = useState(params.initialPage ?? 1);
1623

@@ -65,7 +72,7 @@ export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher,
6572
staleTime: 60_000,
6673
enabled: queriesEnabled && !triggerInfinite,
6774
// Use placeholderData to keep previous data while fetching new page
68-
placeholderData: keepPreviousData ? previousData => previousData : undefined,
75+
placeholderData: keepPreviousData ? KeepPreviousDataFn : undefined,
6976
});
7077

7178
// Infinite mode: accumulate pages

packages/shared/src/react/hooks/useSubscription.rq.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ import type { SubscriptionResult, UseSubscriptionParams } from './useSubscriptio
1414

1515
const HOOK_NAME = 'useSubscription';
1616

17+
/**
18+
* @internal
19+
*/
20+
function KeepPreviousDataFn<Data>(previousData: Data): Data {
21+
return previousData;
22+
}
23+
1724
/**
1825
* This is the new implementation of useSubscription using React Query.
1926
* It is exported only if the package is build with the `CLERK_USE_RQ` environment variable set to `true`.
@@ -36,6 +43,7 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes
3643
const billingEnabled = isOrganization
3744
? environment?.commerceSettings.billing.organization.enabled
3845
: environment?.commerceSettings.billing.user.enabled;
46+
const keepPreviousData = params?.keepPreviousData ?? false;
3947

4048
const [queryClient] = useClerkQueryClient();
4149

@@ -59,7 +67,7 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes
5967
},
6068
staleTime: 1_000 * 60,
6169
enabled: queriesEnabled,
62-
// TODO(@RQ_MIGRATION): Add support for keepPreviousData
70+
placeholderData: keepPreviousData && queriesEnabled ? KeepPreviousDataFn : undefined,
6371
});
6472

6573
const revalidate = useCallback(() => queryClient.invalidateQueries({ queryKey }), [queryClient, queryKey]);

0 commit comments

Comments
 (0)