From 3e5b34e6ebeca00149c9b4639fc2ce56fac5d5b2 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 14 Nov 2025 14:40:26 +0200 Subject: [PATCH 1/7] chore(shared): Improve cache key creation --- .../__tests__/usePagesOrInfinite.spec.ts | 278 ++++++++---------- .../hooks/createBillingPaginatedHook.tsx | 46 ++- .../shared/src/react/hooks/createCacheKeys.ts | 23 ++ packages/shared/src/react/hooks/useAPIKeys.ts | 29 +- .../src/react/hooks/useOrganization.tsx | 115 +++++--- .../src/react/hooks/useOrganizationList.tsx | 94 +++--- .../react/hooks/usePageOrInfinite.types.ts | 107 ++++++- .../src/react/hooks/usePagesOrInfinite.rq.tsx | 73 ++--- .../react/hooks/usePagesOrInfinite.swr.tsx | 30 +- .../src/react/hooks/useSubscription.rq.tsx | 27 +- packages/shared/src/react/types.ts | 10 - 11 files changed, 490 insertions(+), 342 deletions(-) create mode 100644 packages/shared/src/react/hooks/createCacheKeys.ts diff --git a/packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts b/packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts index 2586379fa89..49dfc9cb370 100644 --- a/packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts +++ b/packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts @@ -2,6 +2,7 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createDeferredPromise } from '../../../utils/createDeferredPromise'; +import { createCacheKeys } from '../createCacheKeys'; import { usePagesOrInfinite } from '../usePagesOrInfinite'; import { createMockClerk, createMockQueryClient } from './mocks/clerk'; import { wrapper } from './wrapper'; @@ -12,6 +13,45 @@ const mockClerk = createMockClerk({ queryClient: defaultQueryClient, }); +type ConfigOverrides = Partial<{ + infinite: boolean; + keepPreviousData: boolean; + enabled: boolean; + isSignedIn: boolean; + __experimental_mode: 'cache'; + initialPage: number; + pageSize: number; +}>; + +const buildConfig = ( + params: Params, + overrides: ConfigOverrides = {}, +) => ({ + infinite: overrides.infinite ?? false, + keepPreviousData: overrides.keepPreviousData ?? false, + enabled: overrides.enabled ?? true, + isSignedIn: overrides.isSignedIn, + __experimental_mode: overrides.__experimental_mode, + initialPage: overrides.initialPage ?? params.initialPage ?? 1, + pageSize: overrides.pageSize ?? params.pageSize ?? 10, +}); + +const buildKeys = >( + stablePrefix: string, + params: Params, + tracked: Record = {}, + authenticated = true, +) => + createCacheKeys({ + stablePrefix, + authenticated, + tracked, + untracked: { args: params }, + }); + +const renderUsePagesOrInfinite = (args: { fetcher: any; config: any; keys: any }) => + renderHook(() => usePagesOrInfinite(args as any), { wrapper }); + vi.mock('../../contexts', () => { return { useAssertWrappedByClerkProvider: () => {}, @@ -28,7 +68,7 @@ beforeEach(() => { }); describe('usePagesOrInfinite - basic pagination', () => { - it('uses SWR with merged key and fetcher params; maps data and count', async () => { + it('uses query client with merged key and fetcher params; maps data and count', async () => { const fetcher = vi.fn(async (p: any) => { // simulate API returning paginated response return { @@ -37,14 +77,11 @@ describe('usePagesOrInfinite - basic pagination', () => { }; }); - const params = { initialPage: 2, pageSize: 5 } as const; - const config = { infinite: false, keepPreviousData: true } as const; - const cacheKeys = { type: 't-basic', userId: 'user_123' } as const; + const params = { initialPage: 2, pageSize: 5 }; + const config = buildConfig(params, { keepPreviousData: true }); + const keys = buildKeys('t-basic', params, { userId: 'user_123' }); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); // wait until SWR mock finishes fetching await waitFor(() => expect(result.current.isLoading).toBe(false)); @@ -94,14 +131,11 @@ describe('usePagesOrInfinite - request params and getDifferentKeys', () => { }), ); - const params = { initialPage: 2, pageSize: 3, someFilter: 'A' } as const; - const cacheKeys = { type: 't-params', userId: 'user_42' } as const; - const config = { infinite: false, enabled: true } as const; + const params = { initialPage: 2, pageSize: 3, someFilter: 'A' }; + const config = buildConfig(params); + const keys = buildKeys('t-params', params, { userId: 'user_42' }); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); await waitFor(() => expect(result.current.isLoading).toBe(true)); @@ -139,14 +173,11 @@ describe('usePagesOrInfinite - infinite mode', () => { }); }); - const params = { initialPage: 1, pageSize: 2 } as const; - const config = { infinite: true, keepPreviousData: false, enabled: true } as const; - const cacheKeys = { type: 't-infinite', orgId: 'org_1' } as const; + const params = { initialPage: 1, pageSize: 2 }; + const config = buildConfig(params, { infinite: true }); + const keys = buildKeys('t-infinite', params, { orgId: 'org_1' }); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); // first render should fetch first page await waitFor(() => expect(result.current.isLoading).toBe(false)); @@ -197,14 +228,11 @@ describe('usePagesOrInfinite - disabled and isSignedIn gating', () => { it('does not fetch when enabled=false (pagination mode)', () => { const fetcher = vi.fn(async () => ({ data: [], total_count: 0 })); - const params = { initialPage: 1, pageSize: 3 } as const; - const config = { infinite: false, enabled: false } as const; - const cacheKeys = { type: 't-disabled' } as const; + const params = { initialPage: 1, pageSize: 3 }; + const config = buildConfig(params, { enabled: false }); + const keys = buildKeys('t-disabled', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); // our SWR mock sets loading=false if key is null and not calling fetcher expect(fetcher).toHaveBeenCalledTimes(0); @@ -216,14 +244,11 @@ describe('usePagesOrInfinite - disabled and isSignedIn gating', () => { it('does not fetch when isSignedIn=false (pagination mode)', () => { const fetcher = vi.fn(async () => ({ data: [], total_count: 0 })); - const params = { initialPage: 1, pageSize: 3 } as const; - const config = { infinite: false, enabled: true, isSignedIn: false } as const; - const cacheKeys = { type: 't-signedin-false' } as const; + const params = { initialPage: 1, pageSize: 3 }; + const config = buildConfig(params, { isSignedIn: false }); + const keys = buildKeys('t-signedin-false', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); expect(fetcher).toHaveBeenCalledTimes(0); expect(result.current.data).toEqual([]); @@ -233,11 +258,11 @@ describe('usePagesOrInfinite - disabled and isSignedIn gating', () => { it('does not fetch when isSignedIn=false (infinite mode)', async () => { const fetcher = vi.fn(async () => ({ data: [], total_count: 0 })); - const params = { initialPage: 1, pageSize: 3 } as const; - const config = { infinite: true, enabled: true, isSignedIn: false } as const; - const cacheKeys = { type: 't-signedin-false-inf' } as const; + const params = { initialPage: 1, pageSize: 3 }; + const config = buildConfig(params, { infinite: true, isSignedIn: false }); + const keys = buildKeys('t-signedin-false-inf', params); - const { result } = renderHook(() => usePagesOrInfinite(params, fetcher, config, cacheKeys), { wrapper }); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); expect(fetcher).toHaveBeenCalledTimes(0); expect(result.current.data).toEqual([]); @@ -249,14 +274,11 @@ describe('usePagesOrInfinite - cache mode', () => { it('does not call fetcher in cache mode and allows local setData/revalidate', async () => { const fetcher = vi.fn(async () => ({ data: [{ id: 'remote' }], total_count: 10 })); - const params = { initialPage: 1, pageSize: 3 } as const; - const config = { infinite: false, enabled: true, __experimental_mode: 'cache' as const }; - const cacheKeys = { type: 't-cache', userId: 'u1' } as const; + const params = { initialPage: 1, pageSize: 3 }; + const config = buildConfig(params, { __experimental_mode: 'cache' }); + const keys = buildKeys('t-cache', params, { userId: 'u1' }); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); // Should never be fetching in cache mode expect(result.current.isFetching).toBe(false); @@ -287,14 +309,11 @@ describe('usePagesOrInfinite - keepPreviousData behavior', () => { return deferred.promise.then(() => ({ data: [{ id: 'p2-a' }, { id: 'p2-b' }], total_count: 4 })); }); - const params = { initialPage: 1, pageSize: 2 } as const; - const config = { infinite: false, keepPreviousData: true, enabled: true } as const; - const cacheKeys = { type: 't-keepPrev' } as const; + const params = { initialPage: 1, pageSize: 2 }; + const config = buildConfig(params, { keepPreviousData: true }); + const keys = buildKeys('t-keepPrev', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); expect(result.current.isLoading).toBe(true); await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current.data).toEqual([{ id: 'p1-a' }, { id: 'p1-b' }]); @@ -323,14 +342,11 @@ describe('usePagesOrInfinite - keepPreviousData behavior', () => { return deferred.promise.then(() => ({ data: [{ id: 'p2-a' }, { id: 'p2-b' }], total_count: 4 })); }); - const params = { initialPage: 1, pageSize: 2 } as const; - const config = { infinite: false, keepPreviousData: false, enabled: true } as const; - const cacheKeys = { type: 't-keepPrev' } as const; + const params = { initialPage: 1, pageSize: 2 }; + const config = buildConfig(params, { keepPreviousData: false }); + const keys = buildKeys('t-keepPrev', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); expect(result.current.isLoading).toBe(true); await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current.data).toEqual([{ id: 'p1-a' }, { id: 'p1-b' }]); @@ -359,14 +375,11 @@ describe('usePagesOrInfinite - pagination helpers', () => { total_count: totalCount, })); - const params = { initialPage: 3, pageSize: 5 } as const; - const config = { infinite: false, enabled: true } as const; - const cacheKeys = { type: 't-helpers' } as const; + const params = { initialPage: 3, pageSize: 5 }; + const config = buildConfig(params); + const keys = buildKeys('t-helpers', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); await waitFor(() => expect(result.current.isLoading).toBe(false)); @@ -392,14 +405,11 @@ describe('usePagesOrInfinite - pagination helpers', () => { total_count: totalCount, })); - const params = { initialPage: 1, pageSize: 4 } as const; - const config = { infinite: true, enabled: true } as const; - const cacheKeys = { type: 't-infinite-page' } as const; + const params = { initialPage: 1, pageSize: 4 }; + const config = buildConfig(params, { infinite: true }); + const keys = buildKeys('t-infinite-page', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); await waitFor(() => expect(result.current.isLoading).toBe(false)); @@ -430,14 +440,11 @@ describe('usePagesOrInfinite - behaviors mirrored from useCoreOrganization', () }); }); - const params = { initialPage: 1, pageSize: 2 } as const; - const config = { infinite: false, keepPreviousData: false, enabled: true } as const; - const cacheKeys = { type: 't-core-like-paginated' } as const; + const params = { initialPage: 1, pageSize: 2 }; + const config = buildConfig(params, { keepPreviousData: false }); + const keys = buildKeys('t-core-like-paginated', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); // initial expect(result.current.isLoading).toBe(true); @@ -474,14 +481,11 @@ describe('usePagesOrInfinite - behaviors mirrored from useCoreOrganization', () return deferred.promise.then(() => ({ data: [{ id: '3' }, { id: '4' }], total_count: 4 })); }); - const params = { initialPage: 1, pageSize: 2 } as const; - const config = { infinite: true, keepPreviousData: false, enabled: true } as const; - const cacheKeys = { type: 't-core-like-infinite' } as const; + const params = { initialPage: 1, pageSize: 2 }; + const config = buildConfig(params, { infinite: true, keepPreviousData: false }); + const keys = buildKeys('t-core-like-infinite', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current.isFetching).toBe(false); @@ -515,14 +519,11 @@ describe('usePagesOrInfinite - revalidate behavior', () => { })); }); - const params = { initialPage: 1, pageSize: 2 } as const; - const config = { infinite: false, enabled: true } as const; - const cacheKeys = { type: 't-revalidate-paginated' } as const; + const params = { initialPage: 1, pageSize: 2 }; + const config = buildConfig(params); + const keys = buildKeys('t-revalidate-paginated', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); // Wait for initial load await waitFor(() => expect(result.current.isLoading).toBe(false)); @@ -561,14 +562,11 @@ describe('usePagesOrInfinite - revalidate behavior', () => { })); }); - const params = { initialPage: 1, pageSize: 2 } as const; - const config = { infinite: true, enabled: true } as const; - const cacheKeys = { type: 't-revalidate-infinite' } as const; + const params = { initialPage: 1, pageSize: 2 }; + const config = buildConfig(params, { infinite: true }); + const keys = buildKeys('t-revalidate-infinite', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); // Wait for initial load await waitFor(() => expect(result.current.isLoading).toBe(false)); @@ -607,14 +605,11 @@ describe('usePagesOrInfinite - revalidate behavior', () => { }; }); - const params = { initialPage: 1, pageSize: 2 } as const; - const config = { infinite: true, enabled: true } as const; - const cacheKeys = { type: 't-revalidate-all-pages' } as const; + const params = { initialPage: 1, pageSize: 2 }; + const config = buildConfig(params, { infinite: true }); + const keys = buildKeys('t-revalidate-all-pages', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); // Wait for initial page load await waitFor(() => expect(result.current.isLoading).toBe(false)); @@ -667,14 +662,11 @@ describe('usePagesOrInfinite - error propagation', () => { throw new Error('boom'); }); - const params = { initialPage: 1, pageSize: 2 } as const; - const config = { infinite: false, enabled: true } as const; - const cacheKeys = { type: 't-error' } as const; + const params = { initialPage: 1, pageSize: 2 }; + const config = buildConfig(params); + const keys = buildKeys('t-error', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); await waitFor(() => expect(result.current.isError).toBe(true)); expect(result.current.error).toBeInstanceOf(Error); @@ -684,14 +676,11 @@ describe('usePagesOrInfinite - error propagation', () => { it('sets error and isError in infinite mode when fetcher throws', async () => { const fetcher = vi.fn(() => Promise.reject(new Error('boom2'))); - const params = { initialPage: 1, pageSize: 2 } as const; - const config = { infinite: true, enabled: true } as const; - const cacheKeys = { type: 't-error-inf' } as const; + const params = { initialPage: 1, pageSize: 2 }; + const config = buildConfig(params, { infinite: true }); + const keys = buildKeys('t-error-inf', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); await waitFor(() => expect(result.current.isLoading).toBe(false)); @@ -710,13 +699,15 @@ describe('usePagesOrInfinite - query state transitions and remounting', () => { type TestParams = { initialPage: number; pageSize: number; filter: string }; const params1: TestParams = { initialPage: 1, pageSize: 2, filter: 'A' }; - const config = { infinite: false, enabled: true } as const; - const cacheKeys = { type: 't-transition-test' } as const; // First render with filter 'A' const { result, rerender } = renderHook( ({ params }: { params: TestParams }) => - usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), + usePagesOrInfinite({ + fetcher: fetcher as any, + config: buildConfig(params), + keys: buildKeys('t-transition-test', params), + } as any), { wrapper, initialProps: { params: params1 } }, ); @@ -761,14 +752,11 @@ describe('usePagesOrInfinite - query state transitions and remounting', () => { total_count: 1, })); - const params = { initialPage: 1, pageSize: 2 } as const; - const config = { infinite: false, enabled: true } as const; - const cacheKeys = { type: 't-stable-render' } as const; + const params = { initialPage: 1, pageSize: 2 }; + const config = buildConfig(params); + const keys = buildKeys('t-stable-render', params); - const { result, rerender } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result, rerender } = renderUsePagesOrInfinite({ fetcher, config, keys }); // Wait for initial load await waitFor(() => expect(result.current.isLoading).toBe(false)); @@ -797,14 +785,11 @@ describe('usePagesOrInfinite - query state transitions and remounting', () => { total_count: 2, })); - const params = { initialPage: 1, pageSize: 2 } as const; - const config = { infinite: true, enabled: true } as const; - const cacheKeys = { type: 't-infinite-stable' } as const; + const params = { initialPage: 1, pageSize: 2 }; + const config = buildConfig(params, { infinite: true }); + const keys = buildKeys('t-infinite-stable', params); - const { result, rerender } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result, rerender } = renderUsePagesOrInfinite({ fetcher, config, keys }); // Wait for initial load await waitFor(() => expect(result.current.isLoading).toBe(false)); @@ -831,14 +816,11 @@ describe('usePagesOrInfinite - query state transitions and remounting', () => { return deferred.promise.then(() => ({ data: [{ id: 'second' }], total_count: 1 })); }); - const params = { initialPage: 1, pageSize: 2 } as const; - const config = { infinite: false, enabled: true } as const; - const cacheKeys = { type: 't-loading-vs-fetching' } as const; + const params = { initialPage: 1, pageSize: 2 }; + const config = buildConfig(params); + const keys = buildKeys('t-loading-vs-fetching', params); - const { result } = renderHook( - () => usePagesOrInfinite(params as any, fetcher as any, config as any, cacheKeys as any), - { wrapper }, - ); + const { result } = renderUsePagesOrInfinite({ fetcher, config, keys }); // On initial mount: // - isLoading: true (first fetch, no data) diff --git a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx index 870b0428018..e6c50182762 100644 --- a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx +++ b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx @@ -7,6 +7,7 @@ import { useUserContext, } from '../contexts'; import type { PagesOrInfiniteOptions, PaginatedHookConfig, PaginatedResources } from '../types'; +import { createCacheKeys } from './createCacheKeys'; import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; /** @@ -51,6 +52,16 @@ export function createBillingPaginatedHook & { for?: ForPayerType; @@ -101,28 +112,31 @@ export function createBillingPaginatedHook>( - (hookParams || {}) as TParams, - fetchFn, - { - ...({ - keepPreviousData: safeValues.keepPreviousData, - infinite: safeValues.infinite, - } as PaginatedHookConfig), + const result = usePagesOrInfinite({ + fetcher: fetchFn, + config: { + keepPreviousData: safeValues.keepPreviousData, + infinite: safeValues.infinite, enabled: isEnabled, ...(options?.unauthenticated ? {} : { isSignedIn: Boolean(user) }), __experimental_mode: safeValues.__experimental_mode, + initialPage: safeValues.initialPage, + pageSize: safeValues.pageSize, }, - { - type: resourceType, - ...(options?.unauthenticated - ? { for: safeFor } - : { + keys: createCacheKeys({ + stablePrefix: resourceType, + authenticated: !options?.unauthenticated, + tracked: options?.unauthenticated + ? ({ for: safeFor } as const) + : ({ userId: user?.id, ...(isForOrganization ? { orgId: organization?.id } : {}), - }), - }, - ); + } as const), + untracked: { + args: hookParams as TParams, + }, + }), + }); return result; }; diff --git a/packages/shared/src/react/hooks/createCacheKeys.ts b/packages/shared/src/react/hooks/createCacheKeys.ts new file mode 100644 index 00000000000..ff3f9ba17d8 --- /dev/null +++ b/packages/shared/src/react/hooks/createCacheKeys.ts @@ -0,0 +1,23 @@ +/** + * @internal + */ +export function createCacheKeys< + Params, + StableKey extends string, + T extends Record = Record, + U extends Record | undefined = undefined, +>(params: { + stablePrefix: StableKey; + authenticated: boolean; + tracked: T; + untracked: U extends { args: Params } ? U : never; + + //U extends undefined ? never : U extends { args: Params } ? U['args'] : never; +}) { + return { + queryKey: [params.stablePrefix, params.authenticated, params.tracked, params.untracked] as const, + invalidationKey: [params.stablePrefix, params.authenticated, params.tracked] as const, + stableKey: params.stablePrefix, + authenticated: params.authenticated, + }; +} diff --git a/packages/shared/src/react/hooks/useAPIKeys.ts b/packages/shared/src/react/hooks/useAPIKeys.ts index c090bdf2692..b5302c687fa 100644 --- a/packages/shared/src/react/hooks/useAPIKeys.ts +++ b/packages/shared/src/react/hooks/useAPIKeys.ts @@ -1,9 +1,10 @@ 'use client'; import { eventMethodCalled } from '../../telemetry/events/method-called'; -import type { APIKeyResource, ClerkPaginatedResponse, GetAPIKeysParams } from '../../types'; +import type { APIKeyResource, GetAPIKeysParams } from '../../types'; import { useAssertWrappedByClerkProvider, useClerkInstanceContext } from '../contexts'; import type { PaginatedHookConfig, PaginatedResources } from '../types'; +import { createCacheKeys } from './createCacheKeys'; import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; /** @@ -92,17 +93,25 @@ export function useApiKeys(params?: T): UseApiKeysRe const isEnabled = (safeValues.enabled ?? true) && clerk.loaded; - return usePagesOrInfinite>( - hookParams, - clerk.apiKeys?.getAll ? (params: GetAPIKeysParams) => clerk.apiKeys.getAll(params) : undefined, - { + return usePagesOrInfinite({ + fetcher: clerk.apiKeys?.getAll ? (params: GetAPIKeysParams) => clerk.apiKeys.getAll(params) : undefined, + config: { keepPreviousData: safeValues.keepPreviousData, infinite: safeValues.infinite, enabled: isEnabled, + isSignedIn: Boolean(clerk.user), + initialPage: safeValues.initialPage, + pageSize: safeValues.pageSize, }, - { - type: 'apiKeys', - subject: safeValues.subject || '', - }, - ) as UseApiKeysReturn; + keys: createCacheKeys({ + stablePrefix: 'apiKeys', + authenticated: Boolean(clerk.user), + tracked: { + subject: safeValues.subject, + }, + untracked: { + args: hookParams, + }, + }), + }) as UseApiKeysReturn; } diff --git a/packages/shared/src/react/hooks/useOrganization.tsx b/packages/shared/src/react/hooks/useOrganization.tsx index ff3a7d3c0b9..3def03a8a6c 100644 --- a/packages/shared/src/react/hooks/useOrganization.tsx +++ b/packages/shared/src/react/hooks/useOrganization.tsx @@ -1,7 +1,6 @@ import { getCurrentOrganizationMembership } from '../../organization'; import { eventMethodCalled } from '../../telemetry/events/method-called'; import type { - ClerkPaginatedResponse, GetDomainsParams, GetInvitationsParams, GetMembershipRequestParams, @@ -19,6 +18,7 @@ import { useSessionContext, } from '../contexts'; import type { PaginatedHookConfig, PaginatedResources, PaginatedResourcesWithDefault } from '../types'; +import { createCacheKeys } from './createCacheKeys'; import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; /** @@ -357,70 +357,93 @@ export function useOrganization(params?: T): Us status: invitationsSafeValues.status, }; - const domains = usePagesOrInfinite>( - { - ...domainParams, - }, - organization?.getDomains, - { + const domains = usePagesOrInfinite({ + fetcher: organization?.getDomains, + config: { keepPreviousData: domainSafeValues.keepPreviousData, infinite: domainSafeValues.infinite, enabled: !!domainParams, + isSignedIn: Boolean(organization), + initialPage: domainSafeValues.initialPage, + pageSize: domainSafeValues.pageSize, }, - { - type: 'domains', - organizationId: organization?.id, - }, - ); + keys: createCacheKeys({ + stablePrefix: 'domains', + authenticated: Boolean(organization), + tracked: { + organizationId: organization?.id, + }, + untracked: { + args: domainParams, + }, + }), + }); - const membershipRequests = usePagesOrInfinite< - GetMembershipRequestParams, - ClerkPaginatedResponse - >( - { - ...membershipRequestParams, - }, - organization?.getMembershipRequests, - { + const membershipRequests = usePagesOrInfinite({ + fetcher: organization?.getMembershipRequests, + config: { keepPreviousData: membershipRequestSafeValues.keepPreviousData, infinite: membershipRequestSafeValues.infinite, enabled: !!membershipRequestParams, + isSignedIn: Boolean(organization), + initialPage: membershipRequestSafeValues.initialPage, + pageSize: membershipRequestSafeValues.pageSize, }, - { - type: 'membershipRequests', - organizationId: organization?.id, - }, - ); + keys: createCacheKeys({ + stablePrefix: 'membershipRequests', + authenticated: Boolean(organization), + tracked: { + organizationId: organization?.id, + }, + untracked: { + args: membershipRequestParams, + }, + }), + }); - const memberships = usePagesOrInfinite>( - membersParams || {}, - organization?.getMemberships, - { + const memberships = usePagesOrInfinite({ + fetcher: organization?.getMemberships, + config: { keepPreviousData: membersSafeValues.keepPreviousData, infinite: membersSafeValues.infinite, enabled: !!membersParams, + isSignedIn: Boolean(organization), + initialPage: membersSafeValues.initialPage, + pageSize: membersSafeValues.pageSize, }, - { - type: 'members', - organizationId: organization?.id, - }, - ); + keys: createCacheKeys({ + stablePrefix: 'members', + authenticated: Boolean(organization), + tracked: { + organizationId: organization?.id, + }, + untracked: { + args: membersParams, + }, + }), + }); - const invitations = usePagesOrInfinite>( - { - ...invitationsParams, - }, - organization?.getInvitations, - { + const invitations = usePagesOrInfinite({ + fetcher: organization?.getInvitations, + config: { keepPreviousData: invitationsSafeValues.keepPreviousData, infinite: invitationsSafeValues.infinite, enabled: !!invitationsParams, + isSignedIn: Boolean(organization), + initialPage: invitationsSafeValues.initialPage, + pageSize: invitationsSafeValues.pageSize, }, - { - type: 'invitations', - organizationId: organization?.id, - }, - ); + keys: createCacheKeys({ + stablePrefix: 'invitations', + authenticated: Boolean(organization), + tracked: { + organizationId: organization?.id, + }, + untracked: { + args: invitationsParams, + }, + }), + }); if (organization === undefined) { return { diff --git a/packages/shared/src/react/hooks/useOrganizationList.tsx b/packages/shared/src/react/hooks/useOrganizationList.tsx index f56159c20bf..acf587991a7 100644 --- a/packages/shared/src/react/hooks/useOrganizationList.tsx +++ b/packages/shared/src/react/hooks/useOrganizationList.tsx @@ -1,6 +1,5 @@ import { eventMethodCalled } from '../../telemetry/events/method-called'; import type { - ClerkPaginatedResponse, CreateOrganizationParams, GetUserOrganizationInvitationsParams, GetUserOrganizationMembershipParams, @@ -13,6 +12,7 @@ import type { } from '../../types'; import { useAssertWrappedByClerkProvider, useClerkInstanceContext, useUserContext } from '../contexts'; import type { PaginatedHookConfig, PaginatedResources, PaginatedResourcesWithDefault } from '../types'; +import { createCacheKeys } from './createCacheKeys'; import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; /** @@ -307,60 +307,72 @@ export function useOrganizationList(params? const isClerkLoaded = !!(clerk.loaded && user); - const memberships = usePagesOrInfinite< - GetUserOrganizationMembershipParams, - ClerkPaginatedResponse - >( - userMembershipsParams || {}, - user?.getOrganizationMemberships, - { + const memberships = usePagesOrInfinite({ + fetcher: user?.getOrganizationMemberships, + config: { keepPreviousData: userMembershipsSafeValues.keepPreviousData, infinite: userMembershipsSafeValues.infinite, enabled: !!userMembershipsParams, + isSignedIn: Boolean(user), + initialPage: userMembershipsSafeValues.initialPage, + pageSize: userMembershipsSafeValues.pageSize, }, - { - type: 'userMemberships', - userId: user?.id, - }, - ); + keys: createCacheKeys({ + stablePrefix: 'userMemberships', + authenticated: Boolean(user), + tracked: { + userId: user?.id, + }, + untracked: { + args: userMembershipsParams, + }, + }), + }); - const invitations = usePagesOrInfinite< - GetUserOrganizationInvitationsParams, - ClerkPaginatedResponse - >( - { - ...userInvitationsParams, - }, - user?.getOrganizationInvitations, - { + const invitations = usePagesOrInfinite({ + fetcher: user?.getOrganizationInvitations, + config: { keepPreviousData: userInvitationsSafeValues.keepPreviousData, infinite: userInvitationsSafeValues.infinite, + // In useOrganizationList, you need to opt in by passing an object or `true`. enabled: !!userInvitationsParams, + isSignedIn: Boolean(user), + initialPage: userInvitationsSafeValues.initialPage, + pageSize: userInvitationsSafeValues.pageSize, }, - { - type: 'userInvitations', - userId: user?.id, - }, - ); + keys: createCacheKeys({ + stablePrefix: 'userInvitations', + authenticated: Boolean(user), + tracked: { + userId: user?.id, + }, + untracked: { + args: userInvitationsParams, + }, + }), + }); - const suggestions = usePagesOrInfinite< - GetUserOrganizationSuggestionsParams, - ClerkPaginatedResponse - >( - { - ...userSuggestionsParams, - }, - user?.getOrganizationSuggestions, - { + const suggestions = usePagesOrInfinite({ + fetcher: user?.getOrganizationSuggestions, + config: { keepPreviousData: userSuggestionsSafeValues.keepPreviousData, infinite: userSuggestionsSafeValues.infinite, enabled: !!userSuggestionsParams, + isSignedIn: Boolean(user), + initialPage: userSuggestionsSafeValues.initialPage, + pageSize: userSuggestionsSafeValues.pageSize, }, - { - type: 'userSuggestions', - userId: user?.id, - }, - ); + keys: createCacheKeys({ + stablePrefix: 'userSuggestions', + authenticated: Boolean(user), + tracked: { + userId: user?.id, + }, + untracked: { + args: userSuggestionsParams, + }, + }), + }); // TODO: Properly check for SSR user values if (!isClerkLoaded) { diff --git a/packages/shared/src/react/hooks/usePageOrInfinite.types.ts b/packages/shared/src/react/hooks/usePageOrInfinite.types.ts index 018ea82c75c..a2b702ba79c 100644 --- a/packages/shared/src/react/hooks/usePageOrInfinite.types.ts +++ b/packages/shared/src/react/hooks/usePageOrInfinite.types.ts @@ -4,23 +4,98 @@ export type ArrayType = DataArray extends Array ? export type ExtractData = Type extends { data: infer Data } ? ArrayType : Type; +// export type UsePagesOrInfiniteSignature = < +// Params extends PagesOrInfiniteOptions, +// FetcherReturnData extends Record, +// TCacheKeys extends { +// stableKey: string; +// trackedKeys: { +// [key: string]: unknown; +// args?: Record; +// }; +// untrackedKeys: { +// [key: string]: unknown; +// args?: Record; +// }; +// }, +// CacheKeys extends Record = Record, +// TConfig extends PagesOrInfiniteConfig = PagesOrInfiniteConfig, +// >( +// /** +// * The parameters will be passed to the fetcher. +// */ +// params: Params, +// /** +// * A Promise returning function to fetch your data. +// */ +// fetcher: ((p: Params) => FetcherReturnData | Promise) | undefined, +// acacheKeys: TCacheKeys, +// /** +// * Internal configuration of the hook. +// */ +// config: TConfig, +// cacheKeys: CacheKeys, +// ) => PaginatedResources, TConfig['infinite']>; + +type Config = PagesOrInfiniteConfig & PagesOrInfiniteOptions; + +interface Register { + /** + * Placeholder field to satisfy lint rules; actual shape is provided via declaration merging. + */ + __clerkPaginationQueryKeyArgs?: never; +} + +type AnyQueryKey = Register extends { + queryKey: infer TQueryKey; +} + ? TQueryKey extends ReadonlyArray + ? TQueryKey + : TQueryKey extends Array + ? Readonly + : ReadonlyArray + : ReadonlyArray; + +type QueryArgs = Readonly<{ + args: Params; +}>; + +type QueryKeyWithArgs = readonly [ + string, + boolean, + Record, + QueryArgs, + ...Array, +]; + export type UsePagesOrInfiniteSignature = < - Params extends PagesOrInfiniteOptions, + Params, FetcherReturnData extends Record, - CacheKeys extends Record = Record, - TConfig extends PagesOrInfiniteConfig = PagesOrInfiniteConfig, + TCacheKeys extends { + queryKey: QueryKeyWithArgs; + invalidationKey: AnyQueryKey; + stableKey: string; + }, + // CacheKeys extends Record = Record, + TConfig extends Config = Config, >( - /** - * The parameters will be passed to the fetcher. - */ - params: Params, - /** - * A Promise returning function to fetch your data. - */ - fetcher: ((p: Params) => FetcherReturnData | Promise) | undefined, - /** - * Internal configuration of the hook. - */ - config: TConfig, - cacheKeys: CacheKeys, + // /** + // * The parameters will be passed to the fetcher. + // */ + // params: Params, + // /** + // * A Promise returning function to fetch your data. + // */ + // fetcher: ((p: Params) => FetcherReturnData | Promise) | undefined, + // acacheKeys: TCacheKeys, + // /** + // * Internal configuration of the hook. + // */ + // config: TConfig, + // cacheKeys: CacheKeys, + params: { + fetcher: ((p: Params) => FetcherReturnData | Promise) | undefined; + config: TConfig; + keys: TCacheKeys; + }, ) => PaginatedResources, TConfig['infinite']>; diff --git a/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx b/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx index cdc77b2bed2..abe09c7aeab 100644 --- a/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx +++ b/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx @@ -8,7 +8,7 @@ import { useClerkInfiniteQuery } from '../clerk-rq/useInfiniteQuery'; import { useClerkQuery } from '../clerk-rq/useQuery'; import type { CacheSetter, ValueOrSetter } from '../types'; import type { UsePagesOrInfiniteSignature } from './usePageOrInfinite.types'; -import { getDifferentKeys, useWithSafeValues } from './usePagesOrInfinite.shared'; +import { useWithSafeValues } from './usePagesOrInfinite.shared'; import { usePreviousValue } from './usePreviousValue'; /** @@ -18,12 +18,14 @@ function KeepPreviousDataFn(previousData: Data): Data { return previousData; } -export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher, config, cacheKeys) => { - const [paginatedPage, setPaginatedPage] = useState(params.initialPage ?? 1); +export const usePagesOrInfinite: UsePagesOrInfiniteSignature = params => { + const { fetcher, config, keys } = params; + + const [paginatedPage, setPaginatedPage] = useState(config.initialPage ?? 1); // Cache initialPage and initialPageSize until unmount - const initialPageRef = useRef(params.initialPage ?? 1); - const pageSizeRef = useRef(params.pageSize ?? 10); + const initialPageRef = useRef(config.initialPage ?? 1); + const pageSizeRef = useRef(config.pageSize ?? 10); const enabled = config.enabled ?? true; const isSignedIn = config.isSignedIn; @@ -44,30 +46,35 @@ export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher, // Non-infinite mode: single page query const pagesQueryKey = useMemo(() => { + const [stablePrefix, authenticated, tracked, untracked] = keys.queryKey; + return [ - 'clerk-pages', + stablePrefix, + authenticated, + tracked, { - ...cacheKeys, - ...params, - initialPage: paginatedPage, - pageSize: pageSizeRef.current, + ...untracked, + args: { + ...untracked.args, + initialPage: paginatedPage, + pageSize: pageSizeRef.current, + }, }, - ]; - }, [cacheKeys, params, paginatedPage]); + ] as const; + }, [keys.queryKey, paginatedPage]); const singlePageQuery = useClerkQuery({ queryKey: pagesQueryKey, queryFn: ({ queryKey }) => { - const [, key] = queryKey as [string, Record]; + const { args } = queryKey[3]; if (!fetcher) { return undefined as any; } - const requestParams = getDifferentKeys(key, cacheKeys); - - // @ts-ignore - params type differs slightly but is structurally compatible - return fetcher({ ...params, ...requestParams } as Params); + // Why do we need this ? can we just specify which `args` to use in the key ? + // const requestParams = getDifferentKeys(key, cacheKeys); + return fetcher(args); }, staleTime: 60_000, enabled: queriesEnabled && !triggerInfinite, @@ -77,29 +84,25 @@ export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher, // Infinite mode: accumulate pages const infiniteQueryKey = useMemo(() => { - return [ - 'clerk-pages-infinite', - { - ...cacheKeys, - ...params, - }, - ]; - }, [cacheKeys, params]); + const [stablePrefix, authenticated, tracked, untracked] = keys.queryKey; + + return [stablePrefix + '-inf', authenticated, tracked, untracked] as const; + }, [keys.queryKey]); - const infiniteQuery = useClerkInfiniteQuery>({ + const infiniteQuery = useClerkInfiniteQuery, any, any, typeof infiniteQueryKey, any>({ queryKey: infiniteQueryKey, - initialPageParam: params.initialPage ?? 1, + initialPageParam: config.initialPage ?? 1, getNextPageParam: (lastPage, allPages, lastPageParam) => { const total = lastPage?.total_count ?? 0; - const consumed = (allPages.length + (params.initialPage ? params.initialPage - 1 : 0)) * (params.pageSize ?? 10); + const consumed = (allPages.length + (config.initialPage ? config.initialPage - 1 : 0)) * (config.pageSize ?? 10); return consumed < total ? (lastPageParam as number) + 1 : undefined; }, - queryFn: ({ pageParam }) => { + queryFn: ({ pageParam, queryKey }) => { + const { args } = queryKey[3]; if (!fetcher) { return undefined as any; } - // @ts-ignore - merging page params for fetcher call - return fetcher({ ...params, initialPage: pageParam, pageSize: pageSizeRef.current } as Params); + return fetcher({ ...args, initialPage: pageParam, pageSize: pageSizeRef.current }); }, staleTime: 60_000, enabled: queriesEnabled && triggerInfinite, @@ -119,10 +122,8 @@ export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher, queryClient.removeQueries({ predicate: query => { const key = query.queryKey; - return ( - (Array.isArray(key) && key[0] === 'clerk-pages') || - (Array.isArray(key) && key[0] === 'clerk-pages-infinite') - ); + // Clear all queries that are marked as authenticated + return Array.isArray(key) && key[2] === true; }, }); @@ -216,7 +217,7 @@ export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher, const isLoading = triggerInfinite ? infiniteQuery.isLoading : singlePageQuery.isLoading; const isFetching = triggerInfinite ? infiniteQuery.isFetching : singlePageQuery.isFetching; - const error = (triggerInfinite ? (infiniteQuery.error as any) : singlePageQuery.error) ?? null; + const error = (triggerInfinite ? infiniteQuery.error : singlePageQuery.error) ?? null; const isError = !!error; const fetchNext = useCallback(() => { diff --git a/packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx b/packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx index e23a173835e..1bee4557d16 100644 --- a/packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx +++ b/packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx @@ -34,12 +34,13 @@ const cachingSWRInfiniteOptions = { * * @internal */ -export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher, config, cacheKeys) => { - const [paginatedPage, setPaginatedPage] = useState(params.initialPage ?? 1); +export const usePagesOrInfinite: UsePagesOrInfiniteSignature = params => { + const { fetcher, config, keys } = params; + const [paginatedPage, setPaginatedPage] = useState(config.initialPage ?? 1); // Cache initialPage and initialPageSize until unmount - const initialPageRef = useRef(params.initialPage ?? 1); - const pageSizeRef = useRef(params.pageSize ?? 10); + const initialPageRef = useRef(config.initialPage ?? 1); + const pageSizeRef = useRef(config.pageSize ?? 10); const enabled = config.enabled ?? true; const cacheMode = config.__experimental_mode === 'cache'; @@ -48,8 +49,9 @@ export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher, const isSignedIn = config.isSignedIn; const pagesCacheKey = { - ...cacheKeys, - ...params, + type: keys.queryKey[0], + ...keys.queryKey[2], + ...keys.queryKey[3].args, initialPage: paginatedPage, pageSize: pageSizeRef.current, }; @@ -89,8 +91,9 @@ export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher, if (isSignedIn === false || shouldFetch === false) { return null; } - const requestParams = getDifferentKeys(cacheKeyParams, cacheKeys); - return fetcher({ ...params, ...requestParams }); + const requestParams = getDifferentKeys(cacheKeyParams, { type: keys.queryKey[0], ...keys.queryKey[2] }); + // @ts-ignore - fetcher expects Params subset; narrowing at call-site + return fetcher(requestParams); } : null; @@ -133,17 +136,18 @@ export const usePagesOrInfinite: UsePagesOrInfiniteSignature = (params, fetcher, } return { - ...params, - ...cacheKeys, + type: keys.queryKey[0], + ...keys.queryKey[2], + ...keys.queryKey[3].args, initialPage: initialPageRef.current + pageIndex, pageSize: pageSizeRef.current, }; }, cacheKeyParams => { - // @ts-ignore - remove cache-only keys from request params - const requestParams = getDifferentKeys(cacheKeyParams, cacheKeys); // @ts-ignore - fetcher expects Params subset; narrowing at call-site - return fetcher?.({ ...params, ...requestParams }); + const requestParams = getDifferentKeys(cacheKeyParams, { type: keys.queryKey[0], ...keys.queryKey[2] }); + // @ts-ignore - fetcher expects Params subset; narrowing at call-site + return fetcher?.(requestParams); }, cachingSWRInfiniteOptions, ); diff --git a/packages/shared/src/react/hooks/useSubscription.rq.tsx b/packages/shared/src/react/hooks/useSubscription.rq.tsx index d6a117f27cb..3f6d75e8952 100644 --- a/packages/shared/src/react/hooks/useSubscription.rq.tsx +++ b/packages/shared/src/react/hooks/useSubscription.rq.tsx @@ -21,6 +21,19 @@ function KeepPreviousDataFn(previousData: Data): Data { return previousData; } +export const subscriptionQuery = , U extends Record>(params: { + trackedKeys: T; + untrackedKeys?: U; +}) => { + const stableKey = 'commerce-subscription'; + const { trackedKeys, untrackedKeys } = params; + return { + queryKey: [stableKey, trackedKeys, untrackedKeys] as const, + invalidationKey: [stableKey, trackedKeys] as const, + stableKey, + }; +}; + /** * 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`. @@ -47,14 +60,13 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes const [queryClient] = useClerkQueryClient(); - const queryKey = useMemo(() => { - return [ - 'commerce-subscription', - { + const { queryKey, invalidationKey } = useMemo(() => { + return subscriptionQuery({ + trackedKeys: { userId: user?.id, args: { orgId: isOrganization ? organization?.id : undefined }, }, - ]; + }); }, [user?.id, isOrganization, organization?.id]); const queriesEnabled = Boolean(user?.id && billingEnabled) && (params?.enabled ?? true); @@ -70,7 +82,10 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes placeholderData: keepPreviousData && queriesEnabled ? KeepPreviousDataFn : undefined, }); - const revalidate = useCallback(() => queryClient.invalidateQueries({ queryKey }), [queryClient, queryKey]); + const revalidate = useCallback( + () => queryClient.invalidateQueries({ queryKey: invalidationKey }), + [queryClient, invalidationKey], + ); return { data: query.data, diff --git a/packages/shared/src/react/types.ts b/packages/shared/src/react/types.ts index 93ee8325553..95f0c8e5f87 100644 --- a/packages/shared/src/react/types.ts +++ b/packages/shared/src/react/types.ts @@ -143,14 +143,4 @@ export type PagesOrInfiniteOptions = { * @default 10 */ pageSize?: number; - /** - * On `cache` mode, no request will be triggered when the hook is mounted and the data will be fetched from the cache. - * - * @default undefined - * - * @hidden - * - * @experimental - */ - __experimental_mode?: 'cache'; }; From f981de38c7e5b80b7438bd74c272739a1129b5a0 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 14 Nov 2025 20:34:02 +0200 Subject: [PATCH 2/7] patch --- packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx b/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx index abe09c7aeab..0ac282de43e 100644 --- a/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx +++ b/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx @@ -123,7 +123,7 @@ export const usePagesOrInfinite: UsePagesOrInfiniteSignature = params => { predicate: query => { const key = query.queryKey; // Clear all queries that are marked as authenticated - return Array.isArray(key) && key[2] === true; + return Array.isArray(key) && key[1] === true; }, }); From ba0b3e7669b3c356db149a7d6fc6cc29be2f5603 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 14 Nov 2025 21:09:17 +0200 Subject: [PATCH 3/7] patch --- packages/shared/src/react/hooks/createBillingPaginatedHook.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx index c892bad0aee..8af8b6c3956 100644 --- a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx +++ b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx @@ -139,7 +139,7 @@ export function createBillingPaginatedHook Date: Fri, 14 Nov 2025 21:11:31 +0200 Subject: [PATCH 4/7] changeset --- .changeset/lemon-facts-stare.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lemon-facts-stare.md diff --git a/.changeset/lemon-facts-stare.md b/.changeset/lemon-facts-stare.md new file mode 100644 index 00000000000..42fe6df1113 --- /dev/null +++ b/.changeset/lemon-facts-stare.md @@ -0,0 +1,5 @@ +--- +'@clerk/shared': patch +--- + +Update how cache keys are created in SWR/RQ hooks. From 0c9f8cf79fd0e3cca0aa8b4c510d09f14055dcb8 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 14 Nov 2025 21:18:59 +0200 Subject: [PATCH 5/7] fix --- .../shared/src/react/hooks/createCacheKeys.ts | 2 - .../react/hooks/usePageOrInfinite.types.ts | 60 ++----------------- .../src/react/hooks/usePagesOrInfinite.rq.tsx | 14 ++--- 3 files changed, 11 insertions(+), 65 deletions(-) diff --git a/packages/shared/src/react/hooks/createCacheKeys.ts b/packages/shared/src/react/hooks/createCacheKeys.ts index ff3f9ba17d8..3119107d1c1 100644 --- a/packages/shared/src/react/hooks/createCacheKeys.ts +++ b/packages/shared/src/react/hooks/createCacheKeys.ts @@ -11,8 +11,6 @@ export function createCacheKeys< authenticated: boolean; tracked: T; untracked: U extends { args: Params } ? U : never; - - //U extends undefined ? never : U extends { args: Params } ? U['args'] : never; }) { return { queryKey: [params.stablePrefix, params.authenticated, params.tracked, params.untracked] as const, diff --git a/packages/shared/src/react/hooks/usePageOrInfinite.types.ts b/packages/shared/src/react/hooks/usePageOrInfinite.types.ts index a2b702ba79c..7e71c72e48a 100644 --- a/packages/shared/src/react/hooks/usePageOrInfinite.types.ts +++ b/packages/shared/src/react/hooks/usePageOrInfinite.types.ts @@ -4,39 +4,6 @@ export type ArrayType = DataArray extends Array ? export type ExtractData = Type extends { data: infer Data } ? ArrayType : Type; -// export type UsePagesOrInfiniteSignature = < -// Params extends PagesOrInfiniteOptions, -// FetcherReturnData extends Record, -// TCacheKeys extends { -// stableKey: string; -// trackedKeys: { -// [key: string]: unknown; -// args?: Record; -// }; -// untrackedKeys: { -// [key: string]: unknown; -// args?: Record; -// }; -// }, -// CacheKeys extends Record = Record, -// TConfig extends PagesOrInfiniteConfig = PagesOrInfiniteConfig, -// >( -// /** -// * The parameters will be passed to the fetcher. -// */ -// params: Params, -// /** -// * A Promise returning function to fetch your data. -// */ -// fetcher: ((p: Params) => FetcherReturnData | Promise) | undefined, -// acacheKeys: TCacheKeys, -// /** -// * Internal configuration of the hook. -// */ -// config: TConfig, -// cacheKeys: CacheKeys, -// ) => PaginatedResources, TConfig['infinite']>; - type Config = PagesOrInfiniteConfig & PagesOrInfiniteOptions; interface Register { @@ -76,26 +43,9 @@ export type UsePagesOrInfiniteSignature = < invalidationKey: AnyQueryKey; stableKey: string; }, - // CacheKeys extends Record = Record, TConfig extends Config = Config, ->( - // /** - // * The parameters will be passed to the fetcher. - // */ - // params: Params, - // /** - // * A Promise returning function to fetch your data. - // */ - // fetcher: ((p: Params) => FetcherReturnData | Promise) | undefined, - // acacheKeys: TCacheKeys, - // /** - // * Internal configuration of the hook. - // */ - // config: TConfig, - // cacheKeys: CacheKeys, - params: { - fetcher: ((p: Params) => FetcherReturnData | Promise) | undefined; - config: TConfig; - keys: TCacheKeys; - }, -) => PaginatedResources, TConfig['infinite']>; +>(params: { + fetcher: ((p: Params) => FetcherReturnData | Promise) | undefined; + config: TConfig; + keys: TCacheKeys; +}) => PaginatedResources, TConfig['infinite']>; diff --git a/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx b/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx index 0ac282de43e..e33749ae230 100644 --- a/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx +++ b/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx @@ -72,8 +72,6 @@ export const usePagesOrInfinite: UsePagesOrInfiniteSignature = params => { return undefined as any; } - // Why do we need this ? can we just specify which `args` to use in the key ? - // const requestParams = getDifferentKeys(key, cacheKeys); return fetcher(args); }, staleTime: 60_000, @@ -116,14 +114,14 @@ export const usePagesOrInfinite: UsePagesOrInfiniteSignature = params => { const isNowSignedOut = isSignedIn === false; if (previousIsSignedIn && isNowSignedOut) { - // Clear ALL queries matching the base query keys (including old userId) - // Use predicate to match queries that start with 'clerk-pages' or 'clerk-pages-infinite' - queryClient.removeQueries({ predicate: query => { - const key = query.queryKey; - // Clear all queries that are marked as authenticated - return Array.isArray(key) && key[1] === true; + const [stablePrefix, authenticated] = query.queryKey; + return ( + authenticated === true && + typeof stablePrefix === 'string' && + (stablePrefix === keys.queryKey[0] || stablePrefix === keys.queryKey[0] + '-inf') + ); }, }); From 58f96050813c399e24f198e28547a0c26c11bdd1 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 14 Nov 2025 21:49:17 +0200 Subject: [PATCH 6/7] bundlewatch --- packages/clerk-js/bundlewatch.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index c61b05e0018..62f53a9ba43 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -6,7 +6,7 @@ { "path": "./dist/clerk.legacy.browser.js", "maxSize": "123KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "63.2KB" }, { "path": "./dist/ui-common*.js", "maxSize": "117.1KB" }, - { "path": "./dist/ui-common*.legacy.*.js", "maxSize": "120KB" }, + { "path": "./dist/ui-common*.legacy.*.js", "maxSize": "120.1KB" }, { "path": "./dist/vendors*.js", "maxSize": "47KB" }, { "path": "./dist/coinbase*.js", "maxSize": "38KB" }, { "path": "./dist/stripe-vendors*.js", "maxSize": "1KB" }, From 38da12d73d2083d7fdf290e4ed77e0f6ee5f8bb3 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 14 Nov 2025 22:02:39 +0200 Subject: [PATCH 7/7] fix --- packages/shared/tsdown.config.mts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shared/tsdown.config.mts b/packages/shared/tsdown.config.mts index 6282ea1e89f..499714fd139 100644 --- a/packages/shared/tsdown.config.mts +++ b/packages/shared/tsdown.config.mts @@ -22,6 +22,7 @@ export default defineConfig(({ watch }) => { PACKAGE_VERSION: `"${sharedPackage.version}"`, JS_PACKAGE_VERSION: `"${clerkJsPackage.version}"`, __DEV__: `${watch}`, + __CLERK_USE_RQ__: `${process.env.CLERK_USE_RQ === 'true'}`, }, } satisfies Options;