Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/soft-beers-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/shared': patch
---

Support `keepPreviousData` behaviour in the internal React Query variant of `useSubscription`.
69 changes: 69 additions & 0 deletions packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { createDeferredPromise } from '../../../utils/createDeferredPromise';
import { useSubscription } from '../useSubscription';
import { createMockClerk, createMockOrganization, createMockQueryClient, createMockUser } from './mocks/clerk';
import { wrapper } from './wrapper';
Expand Down Expand Up @@ -144,4 +145,72 @@ describe('useSubscription', () => {
expect(getSubscriptionSpy).toHaveBeenCalledTimes(1);
expect(result.current.isFetching).toBe(false);
});

it('retains previous data while refetching when keepPreviousData=true', async () => {
const { result, rerender } = renderHook(
({ orgId, keepPreviousData }) => {
mockOrganization = createMockOrganization({ id: orgId });
return useSubscription({ for: 'organization', keepPreviousData });
},
{
wrapper,
initialProps: { orgId: 'org_1', keepPreviousData: true },
},
);

await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual({ id: 'sub_org_org_1' });

const deferred = createDeferredPromise();
getSubscriptionSpy.mockImplementationOnce(() => deferred.promise as Promise<{ id: string }>);

rerender({ orgId: 'org_2', keepPreviousData: true });

await waitFor(() => expect(result.current.isFetching).toBe(true));

// Slight difference in behavior between SWR and React Query, but acceptable for the migration.
if (__CLERK_USE_RQ__) {
await waitFor(() => expect(result.current.isLoading).toBe(false));
} else {
await waitFor(() => expect(result.current.isLoading).toBe(true));
}
expect(result.current.data).toEqual({ id: 'sub_org_org_1' });

deferred.resolve({ id: 'sub_org_org_2' });

await waitFor(() => expect(result.current.data).toEqual({ id: 'sub_org_org_2' }));
expect(getSubscriptionSpy).toHaveBeenCalledTimes(2);
});

it('clears data while refetching when keepPreviousData=false', async () => {
const { result, rerender } = renderHook(
({ orgId, keepPreviousData }) => {
mockOrganization = createMockOrganization({ id: orgId });
return useSubscription({ for: 'organization', keepPreviousData });
},
{
wrapper,
initialProps: { orgId: 'org_1', keepPreviousData: false },
},
);

await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.data).toEqual({ id: 'sub_org_org_1' });

const deferred = createDeferredPromise();
getSubscriptionSpy.mockImplementationOnce(() => deferred.promise as Promise<{ id: string }>);

rerender({ orgId: 'org_2', keepPreviousData: false });

await waitFor(() => expect(result.current.isFetching).toBe(true));
expect(result.current.isLoading).toBe(true);
expect(result.current.data).toBeUndefined();

deferred.resolve({ id: 'sub_org_org_2' });

await waitFor(() => expect(result.current.isFetching).toBe(false));
expect(result.current.data).toEqual({ id: 'sub_org_org_2' });
expect(result.current.isLoading).toBe(false);
expect(getSubscriptionSpy).toHaveBeenCalledTimes(2);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,10 @@ export function createBillingPaginatedHook<TResource extends ClerkResource, TPar
(hookParams || {}) as TParams,
fetchFn,
{
keepPreviousData: safeValues.keepPreviousData,
infinite: safeValues.infinite,
...({
keepPreviousData: safeValues.keepPreviousData,
infinite: safeValues.infinite,
} as PaginatedHookConfig<unknown>),
enabled: isEnabled,
...(options?.unauthenticated ? {} : { isSignedIn: Boolean(user) }),
__experimental_mode: safeValues.__experimental_mode,
Expand Down
9 changes: 8 additions & 1 deletion packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import type { UsePagesOrInfiniteSignature } from './usePageOrInfinite.types';
import { getDifferentKeys, useWithSafeValues } from './usePagesOrInfinite.shared';
import { usePreviousValue } from './usePreviousValue';

/**
* @internal
*/
function KeepPreviousDataFn<Data>(previousData: Data): Data {
return previousData;
}

export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher, config, cacheKeys) => {
const [paginatedPage, setPaginatedPage] = useState(params.initialPage ?? 1);

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

// Infinite mode: accumulate pages
Expand Down
10 changes: 9 additions & 1 deletion packages/shared/src/react/hooks/useSubscription.rq.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import type { SubscriptionResult, UseSubscriptionParams } from './useSubscriptio

const HOOK_NAME = 'useSubscription';

/**
* @internal
*/
function KeepPreviousDataFn<Data>(previousData: Data): Data {
return previousData;
}

/**
* This is the new implementation of useSubscription using React Query.
* It is exported only if the package is build with the `CLERK_USE_RQ` environment variable set to `true`.
Expand All @@ -36,6 +43,7 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes
const billingEnabled = isOrganization
? environment?.commerceSettings.billing.organization.enabled
: environment?.commerceSettings.billing.user.enabled;
const keepPreviousData = params?.keepPreviousData ?? false;

const [queryClient] = useClerkQueryClient();

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

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