From fb028ec745a3af24929f925f58c9491f1bccf803 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Nov 2025 17:16:05 +0200 Subject: [PATCH 1/9] wip --- .../hooks/__tests__/useSubscription.spec.tsx | 30 +++++++++++++++++++ .../src/react/hooks/useSubscription.rq.tsx | 7 +++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx b/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx index f4c5e7d0750..1b3702fc94a 100644 --- a/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx +++ b/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx @@ -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'; @@ -144,4 +145,33 @@ 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)); + 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); + }); }); diff --git a/packages/shared/src/react/hooks/useSubscription.rq.tsx b/packages/shared/src/react/hooks/useSubscription.rq.tsx index f5af4c27acf..6d0133a9dab 100644 --- a/packages/shared/src/react/hooks/useSubscription.rq.tsx +++ b/packages/shared/src/react/hooks/useSubscription.rq.tsx @@ -36,6 +36,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(); @@ -49,6 +50,8 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes ]; }, [user?.id, isOrganization, organization?.id]); + const queriesEnabled = Boolean(user?.id && billingEnabled) && ((params as any)?.enabled ?? true); + const query = useClerkQuery({ queryKey, queryFn: ({ queryKey }) => { @@ -56,8 +59,8 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes return clerk.billing.getSubscription(obj.args); }, staleTime: 1_000 * 60, - enabled: Boolean(user?.id && billingEnabled) && ((params as any)?.enabled ?? true), - // TODO(@RQ_MIGRATION): Add support for keepPreviousData + enabled: queriesEnabled, + placeholderData: keepPreviousData && queriesEnabled ? previousData => previousData : undefined, }); const revalidate = useCallback(() => queryClient.invalidateQueries({ queryKey }), [queryClient, queryKey]); From 40a89b10a4f7cd4beafe968229fbd2f8ee6fb4e8 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Nov 2025 17:34:59 +0200 Subject: [PATCH 2/9] wip --- .../hooks/__tests__/useSubscription.spec.tsx | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx b/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx index 1b3702fc94a..d2b0e08d860 100644 --- a/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx +++ b/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx @@ -167,6 +167,7 @@ describe('useSubscription', () => { rerender({ orgId: 'org_2', keepPreviousData: true }); await waitFor(() => expect(result.current.isFetching).toBe(true)); + expect(result.current.isLoading).toBe(false); expect(result.current.data).toEqual({ id: 'sub_org_org_1' }); deferred.resolve({ id: 'sub_org_org_2' }); @@ -174,4 +175,36 @@ describe('useSubscription', () => { 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); + }); }); From ef90340e49ee65da00f18091c09d16f6c8288331 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Nov 2025 17:37:14 +0200 Subject: [PATCH 3/9] wip --- packages/shared/src/react/hooks/useSubscription.rq.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/react/hooks/useSubscription.rq.tsx b/packages/shared/src/react/hooks/useSubscription.rq.tsx index 6d0133a9dab..11dcbbc2f4f 100644 --- a/packages/shared/src/react/hooks/useSubscription.rq.tsx +++ b/packages/shared/src/react/hooks/useSubscription.rq.tsx @@ -12,7 +12,7 @@ import { } from '../contexts'; import type { SubscriptionResult, UseSubscriptionParams } from './useSubscription.types'; -const hookName = 'useSubscription'; +const HOOK_NAME = 'useSubscription'; /** * This is the new implementation of useSubscription using React Query. @@ -21,7 +21,7 @@ const hookName = 'useSubscription'; * @internal */ export function useSubscription(params?: UseSubscriptionParams): SubscriptionResult { - useAssertWrappedByClerkProvider(hookName); + useAssertWrappedByClerkProvider(HOOK_NAME); const clerk = useClerkInstanceContext(); const user = useUserContext(); @@ -30,7 +30,7 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes // @ts-expect-error `__unstable__environment` is not typed const environment = clerk.__unstable__environment as unknown as EnvironmentResource | null | undefined; - clerk.telemetry?.record(eventMethodCalled(hookName)); + clerk.telemetry?.record(eventMethodCalled(HOOK_NAME)); const isOrganization = params?.for === 'organization'; const billingEnabled = isOrganization From 012eaa51fe39be5041cc6d8d0d4eefaca664cba4 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Nov 2025 18:08:36 +0200 Subject: [PATCH 4/9] wpow --- packages/shared/src/react/types.ts | 35 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/shared/src/react/types.ts b/packages/shared/src/react/types.ts index 93ee8325553..822e81bc74c 100644 --- a/packages/shared/src/react/types.ts +++ b/packages/shared/src/react/types.ts @@ -86,20 +86,27 @@ export type PaginatedResourcesWithDefault = { /** * @inline */ -export type PaginatedHookConfig = T & { - /** - * If `true`, newly fetched data will be appended to the existing list rather than replacing it. Useful for implementing infinite scroll functionality. - * - * @default false - */ - infinite?: boolean; - /** - * If `true`, the previous data will be kept in the cache until new data is fetched. - * - * @default false - */ - keepPreviousData?: boolean; -}; +export type PaginatedHookConfig = T & + ( + | { + /** + * If `true`, newly fetched data will be appended to the existing list rather than replacing it. Useful for implementing infinite scroll functionality. + * + * @default false + */ + infinite?: boolean; + keepPreviousData?: never; + } + | { + /** + * If `true`, the previous data will be kept in the cache until new data is fetched. + * + * @default false + */ + keepPreviousData?: boolean; + infinite?: never; + } + ); export type PagesOrInfiniteConfig = PaginatedHookConfig<{ /** From 7a1a5fe97e965e5a4590f475fa1900f87234342f Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Nov 2025 18:21:29 +0200 Subject: [PATCH 5/9] wip --- .../react/hooks/createBillingPaginatedHook.tsx | 6 ++++-- packages/shared/src/react/types.ts | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx index 7e77f165047..f172fb37a7c 100644 --- a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx +++ b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx @@ -96,8 +96,10 @@ export function createBillingPaginatedHook), enabled: isEnabled, ...(options?.unauthenticated ? {} : { isSignedIn: Boolean(user) }), __experimental_mode: safeValues.__experimental_mode, diff --git a/packages/shared/src/react/types.ts b/packages/shared/src/react/types.ts index 822e81bc74c..80172f6260f 100644 --- a/packages/shared/src/react/types.ts +++ b/packages/shared/src/react/types.ts @@ -94,17 +94,27 @@ export type PaginatedHookConfig = T & * * @default false */ - infinite?: boolean; - keepPreviousData?: never; + infinite?: false; + /** + * If `true`, the previous data will be kept in the cache until new data is fetched. + * + * @default false + */ + keepPreviousData?: boolean; } | { + /** + * If `true`, newly fetched data will be appended to the existing list rather than replacing it. Useful for implementing infinite scroll functionality. + * + * @default false + */ + infinite?: boolean; /** * If `true`, the previous data will be kept in the cache until new data is fetched. * * @default false */ - keepPreviousData?: boolean; - infinite?: never; + keepPreviousData?: false; } ); From eba92028f03e4ef80f78af2af982033a82a7c53f Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Nov 2025 18:40:26 +0200 Subject: [PATCH 6/9] enabled --- .../createBillingPaginatedHook.spec.tsx | 20 +++++++++++++++++++ .../hooks/createBillingPaginatedHook.tsx | 15 +++++++++++--- .../src/react/hooks/useSubscription.rq.tsx | 2 +- .../src/react/hooks/useSubscription.swr.tsx | 3 ++- .../src/react/hooks/useSubscription.types.ts | 6 ++++++ 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx b/packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx index d916bc2d856..1ff26e757fd 100644 --- a/packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx +++ b/packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx @@ -98,6 +98,26 @@ describe('createBillingPaginatedHook', () => { expect(result.current.isFetching).toBe(false); }); + it('does not fetch when enabled is false', () => { + const { result } = renderHook(() => useDummyAuth({ initialPage: 1, pageSize: 4, enabled: false }), { wrapper }); + + expect(useFetcherMock).toHaveBeenCalledWith('user'); + + expect(fetcherMock).not.toHaveBeenCalled(); + expect(result.current.isLoading).toBe(false); + expect(result.current.isFetching).toBe(false); + expect(result.current.data).toEqual([]); + }); + + it('fetches when enabled is true', async () => { + const { result } = renderHook(() => useDummyAuth({ initialPage: 1, pageSize: 4, enabled: true }), { wrapper }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + expect(useFetcherMock).toHaveBeenCalledWith('user'); + expect(fetcherMock).toHaveBeenCalled(); + expect(fetcherMock.mock.calls[0][0]).toStrictEqual({ initialPage: 1, pageSize: 4 }); + }); + it('authenticated hook: does not fetch when user is null', () => { mockUser = null; diff --git a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx index f172fb37a7c..870b0428018 100644 --- a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx +++ b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx @@ -43,14 +43,23 @@ export function createBillingPaginatedHook) { - type HookParams = PaginatedHookConfig & { + type HookParams = PaginatedHookConfig< + PagesOrInfiniteOptions & { + /** + * If `true`, a request will be triggered when the hook is mounted. + * + * @default true + */ + enabled?: boolean; + } + > & { for?: ForPayerType; }; return function useBillingHook( params?: T, ): PaginatedResources { - const { for: _for, ...paginationParams } = params || ({} as Partial); + const { for: _for, enabled: externalEnabled, ...paginationParams } = params || ({} as Partial); const safeFor = _for || 'user'; @@ -90,7 +99,7 @@ export function createBillingPaginatedHook>( (hookParams || {}) as TParams, diff --git a/packages/shared/src/react/hooks/useSubscription.rq.tsx b/packages/shared/src/react/hooks/useSubscription.rq.tsx index 11dcbbc2f4f..0ae9393cab0 100644 --- a/packages/shared/src/react/hooks/useSubscription.rq.tsx +++ b/packages/shared/src/react/hooks/useSubscription.rq.tsx @@ -50,7 +50,7 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes ]; }, [user?.id, isOrganization, organization?.id]); - const queriesEnabled = Boolean(user?.id && billingEnabled) && ((params as any)?.enabled ?? true); + const queriesEnabled = Boolean(user?.id && billingEnabled) && (params?.enabled ?? true); const query = useClerkQuery({ queryKey, diff --git a/packages/shared/src/react/hooks/useSubscription.swr.tsx b/packages/shared/src/react/hooks/useSubscription.swr.tsx index 2c928d162eb..792074818b9 100644 --- a/packages/shared/src/react/hooks/useSubscription.swr.tsx +++ b/packages/shared/src/react/hooks/useSubscription.swr.tsx @@ -35,9 +35,10 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes const billingEnabled = isOrganization ? environment?.commerceSettings.billing.organization.enabled : environment?.commerceSettings.billing.user.enabled; + const isEnabled = (params?.enabled ?? true) && billingEnabled; const swr = useSWR( - billingEnabled + isEnabled ? { type: 'commerce-subscription', userId: user?.id, diff --git a/packages/shared/src/react/hooks/useSubscription.types.ts b/packages/shared/src/react/hooks/useSubscription.types.ts index 0ead84085bc..06db9edc211 100644 --- a/packages/shared/src/react/hooks/useSubscription.types.ts +++ b/packages/shared/src/react/hooks/useSubscription.types.ts @@ -7,6 +7,12 @@ export type UseSubscriptionParams = { * Defaults to false. */ keepPreviousData?: boolean; + /** + * If `true`, a request will be triggered when the hook is mounted. + * + * @default true + */ + enabled?: boolean; }; export type SubscriptionResult = { From 88e40516599d71b9d53d1a4a20138310c99501b3 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 11 Nov 2025 19:07:50 +0200 Subject: [PATCH 7/9] enabled 2 --- .../__tests__/OrganizationProfile.test.tsx | 9 ++-- .../src/ui/contexts/components/Plans.tsx | 54 +++++++++---------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx index 50ad3755a7e..3a4d3b0b46a 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx @@ -85,8 +85,8 @@ describe('OrganizationProfile', () => { render(, { wrapper }); await waitFor(() => expect(screen.queryByText('Billing')).toBeNull()); - expect(fixtures.clerk.billing.getSubscription).toHaveBeenCalled(); - expect(fixtures.clerk.billing.getStatements).toHaveBeenCalled(); + expect(fixtures.clerk.billing.getSubscription).not.toHaveBeenCalled(); + expect(fixtures.clerk.billing.getStatements).not.toHaveBeenCalled(); }); it('does not include Billing when missing billing permission even with paid plans', async () => { @@ -109,9 +109,8 @@ describe('OrganizationProfile', () => { render(, { wrapper }); await waitFor(() => expect(screen.queryByText('Billing')).toBeNull()); - // TODO(@RQ_MIGRATION): Offer a way to disable these, because they fire unnecessary requests. - expect(fixtures.clerk.billing.getSubscription).toHaveBeenCalled(); - expect(fixtures.clerk.billing.getStatements).toHaveBeenCalled(); + expect(fixtures.clerk.billing.getSubscription).not.toHaveBeenCalled(); + expect(fixtures.clerk.billing.getStatements).not.toHaveBeenCalled(); }); it('does not include Billing when organization billing is disabled', async () => { const { wrapper, fixtures } = await createFixtures(f => { diff --git a/packages/clerk-js/src/ui/contexts/components/Plans.tsx b/packages/clerk-js/src/ui/contexts/components/Plans.tsx index 73afd6eeb0c..489ef63b71b 100644 --- a/packages/clerk-js/src/ui/contexts/components/Plans.tsx +++ b/packages/clerk-js/src/ui/contexts/components/Plans.tsx @@ -5,6 +5,7 @@ import { __experimental_useStatements, __experimental_useSubscription, useClerk, + useOrganization, useSession, } from '@clerk/shared/react'; import type { @@ -15,6 +16,7 @@ import type { } from '@clerk/shared/types'; import { useCallback, useMemo } from 'react'; +import { useProtect } from '@/ui/common/Gate'; import { getClosestProfileScrollBox } from '@/ui/utils/getClosestProfileScrollBox'; import type { LocalizationKey } from '../../localization'; @@ -28,44 +30,42 @@ export function normalizeFormatted(formatted: string) { return formatted.endsWith('.00') ? formatted.slice(0, -3) : formatted; } -// TODO(@COMMERCE): Rename payment sources to payment methods at the API level -export const usePaymentMethods = () => { +const useBillingHookParams = () => { const subscriberType = useSubscriberTypeContext(); - return __experimental_usePaymentMethods({ + const { organization } = useOrganization(); + const allowBillingRoutes = useProtect( + has => + has({ + permission: 'org:sys_billing:read', + }) || has({ permission: 'org:sys_billing:manage' }), + ); + + return { for: subscriberType, - initialPage: 1, - pageSize: 10, keepPreviousData: true, - }); + // If the user is in an organization, only fetch billing data if they have the necessary permissions + enabled: organization ? allowBillingRoutes : true, + }; +}; + +export const usePaymentMethods = () => { + const params = useBillingHookParams(); + return __experimental_usePaymentMethods(params); }; export const usePaymentAttempts = () => { - const subscriberType = useSubscriberTypeContext(); - return __experimental_usePaymentAttempts({ - for: subscriberType, - initialPage: 1, - pageSize: 10, - keepPreviousData: true, - }); + const params = useBillingHookParams(); + return __experimental_usePaymentAttempts(params); }; -export const useStatements = (params?: { mode: 'cache' }) => { - const subscriberType = useSubscriberTypeContext(); - return __experimental_useStatements({ - for: subscriberType, - initialPage: 1, - pageSize: 10, - keepPreviousData: true, - __experimental_mode: params?.mode, - }); +export const useStatements = (externalParams?: { mode: 'cache' }) => { + const params = useBillingHookParams(); + return __experimental_useStatements({ ...params, __experimental_mode: externalParams?.mode }); }; export const useSubscription = () => { - const subscriberType = useSubscriberTypeContext(); - const subscription = __experimental_useSubscription({ - for: subscriberType, - keepPreviousData: true, - }); + const params = useBillingHookParams(); + const subscription = __experimental_useSubscription(params); const subscriptionItems = useMemo( () => subscription.data?.subscriptionItems || [], [subscription.data?.subscriptionItems], From ad91c3943f63ba9f017b32a14408c246bd04c580 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 13 Nov 2025 13:07:15 +0200 Subject: [PATCH 8/9] cleanup --- .changeset/soft-beers-sit.md | 5 +++ .../src/react/hooks/usePagesOrInfinite.rq.tsx | 9 +++- .../src/react/hooks/useSubscription.rq.tsx | 9 +++- packages/shared/src/react/types.ts | 45 ++++++------------- 4 files changed, 35 insertions(+), 33 deletions(-) create mode 100644 .changeset/soft-beers-sit.md diff --git a/.changeset/soft-beers-sit.md b/.changeset/soft-beers-sit.md new file mode 100644 index 00000000000..35701cc0040 --- /dev/null +++ b/.changeset/soft-beers-sit.md @@ -0,0 +1,5 @@ +--- +'@clerk/shared': patch +--- + +Support `keepPreviousData` behaviour in the internal React Query variant of `useSubscription`. diff --git a/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx b/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx index c7a2770b037..cdc77b2bed2 100644 --- a/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx +++ b/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx @@ -11,6 +11,13 @@ import type { UsePagesOrInfiniteSignature } from './usePageOrInfinite.types'; import { getDifferentKeys, useWithSafeValues } from './usePagesOrInfinite.shared'; import { usePreviousValue } from './usePreviousValue'; +/** + * @internal + */ +function KeepPreviousDataFn(previousData: Data): Data { + return previousData; +} + export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher, config, cacheKeys) => { const [paginatedPage, setPaginatedPage] = useState(params.initialPage ?? 1); @@ -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 diff --git a/packages/shared/src/react/hooks/useSubscription.rq.tsx b/packages/shared/src/react/hooks/useSubscription.rq.tsx index 0ae9393cab0..d6a117f27cb 100644 --- a/packages/shared/src/react/hooks/useSubscription.rq.tsx +++ b/packages/shared/src/react/hooks/useSubscription.rq.tsx @@ -14,6 +14,13 @@ import type { SubscriptionResult, UseSubscriptionParams } from './useSubscriptio const HOOK_NAME = 'useSubscription'; +/** + * @internal + */ +function KeepPreviousDataFn(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`. @@ -60,7 +67,7 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes }, staleTime: 1_000 * 60, enabled: queriesEnabled, - placeholderData: keepPreviousData && queriesEnabled ? previousData => previousData : undefined, + placeholderData: keepPreviousData && queriesEnabled ? KeepPreviousDataFn : undefined, }); const revalidate = useCallback(() => queryClient.invalidateQueries({ queryKey }), [queryClient, queryKey]); diff --git a/packages/shared/src/react/types.ts b/packages/shared/src/react/types.ts index 80172f6260f..93ee8325553 100644 --- a/packages/shared/src/react/types.ts +++ b/packages/shared/src/react/types.ts @@ -86,37 +86,20 @@ export type PaginatedResourcesWithDefault = { /** * @inline */ -export type PaginatedHookConfig = T & - ( - | { - /** - * If `true`, newly fetched data will be appended to the existing list rather than replacing it. Useful for implementing infinite scroll functionality. - * - * @default false - */ - infinite?: false; - /** - * If `true`, the previous data will be kept in the cache until new data is fetched. - * - * @default false - */ - keepPreviousData?: boolean; - } - | { - /** - * If `true`, newly fetched data will be appended to the existing list rather than replacing it. Useful for implementing infinite scroll functionality. - * - * @default false - */ - infinite?: boolean; - /** - * If `true`, the previous data will be kept in the cache until new data is fetched. - * - * @default false - */ - keepPreviousData?: false; - } - ); +export type PaginatedHookConfig = T & { + /** + * If `true`, newly fetched data will be appended to the existing list rather than replacing it. Useful for implementing infinite scroll functionality. + * + * @default false + */ + infinite?: boolean; + /** + * If `true`, the previous data will be kept in the cache until new data is fetched. + * + * @default false + */ + keepPreviousData?: boolean; +}; export type PagesOrInfiniteConfig = PaginatedHookConfig<{ /** From 85a1cb5df913296cb60c5f71334d105558af5a10 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 13 Nov 2025 13:25:57 +0200 Subject: [PATCH 9/9] fix test for swr variant --- .../src/react/hooks/__tests__/useSubscription.spec.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx b/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx index d2b0e08d860..96cd2870280 100644 --- a/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx +++ b/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx @@ -167,7 +167,13 @@ describe('useSubscription', () => { rerender({ orgId: 'org_2', keepPreviousData: true }); await waitFor(() => expect(result.current.isFetching).toBe(true)); - expect(result.current.isLoading).toBe(false); + + // 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' });