From a28fa4dc63804fad317139db1603a9f8002dfc33 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 10 Sep 2025 20:43:51 +0300 Subject: [PATCH 1/8] feat(clerk-js): Lazy query client --- packages/clerk-js/package.json | 1 + packages/clerk-js/src/core/clerk.ts | 16 ++++ packages/clerk-js/src/core/query-core.ts | 3 + .../ui/contexts/CoreClerkContextWrapper.tsx | 2 + .../src/contexts/ClerkContextProvider.tsx | 23 ++++- packages/react/src/isomorphicClerk.ts | 87 +++++++++++++++++++ packages/shared/package.json | 1 + packages/shared/src/react/contexts.tsx | 24 +++-- .../src/react/hooks/useSubscription.tsx | 62 ++++++++++++- 9 files changed, 207 insertions(+), 12 deletions(-) create mode 100644 packages/clerk-js/src/core/query-core.ts diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index d1afe83202b..e2bda0b686d 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -71,6 +71,7 @@ "@formkit/auto-animate": "^0.8.2", "@stripe/stripe-js": "5.6.0", "@swc/helpers": "^0.5.17", + "@tanstack/query-core": "^5.87.4", "@zxcvbn-ts/core": "3.0.4", "@zxcvbn-ts/language-common": "3.0.4", "alien-signals": "2.0.6", diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d9c3d53aef8..a1b65779b5f 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -95,6 +95,7 @@ import type { WaitlistResource, Web3Provider, } from '@clerk/types'; +import type { QueryClient } from '@tanstack/query-core'; import { debugLogger, initDebugLogger } from '@/utils/debug'; @@ -222,6 +223,7 @@ export class Clerk implements ClerkInterface { // converted to protected environment to support `updateEnvironment` type assertion protected environment?: EnvironmentResource | null; + #queryClient: QueryClient | undefined; #publishableKey = ''; #domain: DomainOrProxyUrl['domain']; #proxyUrl: DomainOrProxyUrl['proxyUrl']; @@ -240,6 +242,20 @@ export class Clerk implements ClerkInterface { #touchThrottledUntil = 0; #publicEventBus = createClerkEventBus(); + get __internal_queryClient(): QueryClient | undefined { + return this.#queryClient; + } + + public async getInternalQueryClient(): Promise { + const QueryClient = await import('./query-core').then(module => module.QueryClient); + if (!this.#queryClient) { + this.#queryClient = new QueryClient(); + // @ts-expect-error - queryClientStatus is not typed + this.#publicEventBus.emit('queryClientStatus', 'ready'); + } + return this.#queryClient; + } + public __internal_getCachedResources: | (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>) | undefined; diff --git a/packages/clerk-js/src/core/query-core.ts b/packages/clerk-js/src/core/query-core.ts new file mode 100644 index 00000000000..71a5e77cc2d --- /dev/null +++ b/packages/clerk-js/src/core/query-core.ts @@ -0,0 +1,3 @@ +import { QueryClient } from '@tanstack/query-core'; + +export { QueryClient }; diff --git a/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx b/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx index b4389dc9363..1c781c47c66 100644 --- a/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx +++ b/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx @@ -52,6 +52,8 @@ export function CoreClerkContextWrapper(props: CoreClerkContextWrapperProps): JS diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index f38f9f785f5..74b472bb903 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -88,12 +88,33 @@ export function ClerkContextProvider(props: ClerkContextProvider) { return { value }; }, [orgId, organization]); + const [queryStatus, setQueryStatus] = React.useState('loading'); + + React.useEffect(() => { + // @ts-expect-error - queryClientStatus is not typed + clerk.on('queryClientStatus', setQueryStatus); + return () => { + // @ts-expect-error - queryClientStatus is not typed + clerk.off('queryClientStatus', setQueryStatus); + }; + }, [clerk]); + + const queryClient = React.useMemo(() => { + return clerk.__internal_queryClient; + }, [queryStatus, clerkStatus]); + + console.log('queryStatus', queryStatus, queryClient); + return ( // @ts-expect-error value passed is of type IsomorphicClerk where the context expects LoadedClerk - + = { + get(_target, prop) { + // Avoid being treated as a Promise/thenable by test runners or frameworks + if (prop === 'then') { + return undefined; + } + if (prop === 'toString') { + return () => `[${label}]`; + } + if (prop === Symbol.toPrimitive) { + return () => 0; + } + return self; + }, + apply() { + return self; + }, + construct() { + return self as unknown as object; + }, + has() { + return true; + }, + set() { + return true; + }, + }; + + self = new Proxy(callableTarget, handler) as unknown as RecursiveMock; + return self; +} + +/** + * Returns a permissive mock compatible with `QueryClient` usage in tests. + * It accepts any chain of property accesses and calls without throwing. + */ +export function createMockQueryClient(): RecursiveMock { + return createRecursiveProxy('MockQueryClient') as unknown as RecursiveMock; +} + export class IsomorphicClerk implements IsomorphicLoadedClerk { private readonly mode: 'browser' | 'server'; private readonly options: IsomorphicClerkOptions; @@ -146,6 +206,8 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { private premountApiKeysNodes = new Map(); private premountOAuthConsentNodes = new Map(); private premountTaskChooseOrganizationNodes = new Map(); + private prefetchQueryClientStatus = false; + // A separate Map of `addListener` method calls to handle multiple listeners. private premountAddListenerCalls = new Map< ListenerCallback, @@ -162,6 +224,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { #publishableKey: string; #eventBus = createClerkEventBus(); #stateProxy: StateProxy; + #__internal_queryClient = createMockQueryClient(); get publishableKey(): string { return this.#publishableKey; @@ -283,6 +346,18 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { return this.clerkjs?.isStandardBrowser || this.options.standardBrowser || false; } + get __internal_queryClient() { + // @ts-expect-error - __internal_queryClient is not typed + if (!this.clerkjs?.__internal_queryClient) { + // @ts-expect-error - __internal_queryClient is not typed + void this.clerkjs?.getInternalQueryClient?.(); + this.prefetchQueryClientStatus = true; + } + + // @ts-expect-error - __internal_queryClient is not typed + return this.clerkjs?.__internal_queryClient || this.#__internal_queryClient; + } + get isSatellite() { // This getter can run in environments where window is not available. // In those cases we should expect and use domain as a string @@ -567,6 +642,13 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { this.on('status', listener, { notify: true }); }); + // @ts-expect-error - queryClientStatus is not typed + this.#eventBus.internal.retrieveListeners('queryClientStatus')?.forEach(listener => { + // Since clerkjs exists it will call `this.clerkjs.on('status', listener)` + // @ts-expect-error - queryClientStatus is not typed + this.on('queryClientStatus', listener, { notify: true }); + }); + if (this.preopenSignIn !== null) { clerkjs.openSignIn(this.preopenSignIn); } @@ -611,6 +693,11 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { clerkjs.openWaitlist(this.preOpenWaitlist); } + if (this.prefetchQueryClientStatus) { + // @ts-expect-error - queryClientStatus is not typed + this.clerkjs.getInternalQueryClient?.(); + } + this.premountSignInNodes.forEach((props, node) => { clerkjs.mountSignIn(node, props); }); diff --git a/packages/shared/package.json b/packages/shared/package.json index 543ab746414..a685a8abc1a 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -146,6 +146,7 @@ }, "dependencies": { "@clerk/types": "workspace:^", + "@tanstack/react-query": "^5.87.4", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", diff --git a/packages/shared/src/react/contexts.tsx b/packages/shared/src/react/contexts.tsx index c54ea6f7a2a..0a863e232c0 100644 --- a/packages/shared/src/react/contexts.tsx +++ b/packages/shared/src/react/contexts.tsx @@ -10,6 +10,7 @@ import type { SignedInSessionResource, UserResource, } from '@clerk/types'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { PropsWithChildren } from 'react'; import React from 'react'; @@ -59,22 +60,27 @@ const OrganizationProvider = ({ children, organization, swrConfig, + queryClient, }: PropsWithChildren< OrganizationContextProps & { // Exporting inferred types directly from SWR will result in error while building declarations swrConfig?: any; + queryClient?: QueryClient; } >) => { + const [defaultClient] = React.useState(() => new QueryClient()); return ( - - - {children} - - + + + + {children} + + + ); }; diff --git a/packages/shared/src/react/hooks/useSubscription.tsx b/packages/shared/src/react/hooks/useSubscription.tsx index 53c48df8c08..76bce1fbee5 100644 --- a/packages/shared/src/react/hooks/useSubscription.tsx +++ b/packages/shared/src/react/hooks/useSubscription.tsx @@ -1,5 +1,6 @@ import type { EnvironmentResource, ForPayerType } from '@clerk/types'; -import { useCallback } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useCallback, useMemo } from 'react'; import { eventMethodCalled } from '../../telemetry/events'; import { useSWR } from '../clerk-swr'; @@ -29,7 +30,7 @@ type UseSubscriptionParams = { * * @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change. It is advised to [pin](https://clerk.com/docs/pinning) the SDK version and the clerk-js version to avoid breaking changes. */ -export const useSubscription = (params?: UseSubscriptionParams) => { +export const useSubscriptionPrev = (params?: UseSubscriptionParams) => { useAssertWrappedByClerkProvider(hookName); const clerk = useClerkInstanceContext(); @@ -78,3 +79,60 @@ export const useSubscription = (params?: UseSubscriptionParams) => { revalidate, }; }; + +export const useSubscription = (params?: UseSubscriptionParams) => { + useAssertWrappedByClerkProvider(hookName); + + const clerk = useClerkInstanceContext(); + const user = useUserContext(); + const { organization } = useOrganizationContext(); + + // console.log('cache', cache); + + // @ts-expect-error `__unstable__environment` is not typed + const environment = clerk.__unstable__environment as unknown as EnvironmentResource | null | undefined; + + clerk.telemetry?.record(eventMethodCalled(hookName)); + + const isOrganization = params?.for === 'organization'; + const billingEnabled = isOrganization + ? environment?.commerceSettings.billing.organization.enabled + : environment?.commerceSettings.billing.user.enabled; + + const queryClient = useQueryClient(); + + const queryKey = useMemo(() => { + return [ + 'commerce-subscription', + { + userId: user?.id, + args: { orgId: isOrganization ? organization?.id : undefined }, + }, + ]; + }, [user?.id, isOrganization, organization?.id]); + + const query = useQuery({ + queryKey, + queryFn: ({ queryKey }) => { + const obj = queryKey[1] as { + args: { + orgId?: string; + }; + }; + return clerk.billing.getSubscription(obj.args); + }, + staleTime: 1_0000 * 60, + enabled: Boolean(user?.id && billingEnabled) && clerk.status === 'ready', + // placeholderData + }); + + const revalidate = useCallback(() => queryClient.invalidateQueries({ queryKey }), [queryClient, queryKey]); + + return { + data: query.data, + error: query.error, + isLoading: query.isLoading, + isFetching: query.isFetching, + revalidate, + }; +}; From a3ad75cab44ebfeca9a9c852031ddf95fb58fa8e Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 19 Sep 2025 11:07:35 +0300 Subject: [PATCH 2/8] wip --- .../ui/contexts/CoreClerkContextWrapper.tsx | 4 +- .../src/contexts/ClerkContextProvider.tsx | 4 +- packages/shared/src/react/contexts.tsx | 26 ++++----- .../src/react/hooks/useSubscription.tsx | 55 ++++++++++++++----- 4 files changed, 55 insertions(+), 34 deletions(-) diff --git a/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx b/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx index 1c781c47c66..81abd3021de 100644 --- a/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx +++ b/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx @@ -52,8 +52,8 @@ export function CoreClerkContextWrapper(props: CoreClerkContextWrapperProps): JS diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index 74b472bb903..3852f35ad01 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -103,7 +103,7 @@ export function ClerkContextProvider(props: ClerkContextProvider) { return clerk.__internal_queryClient; }, [queryStatus, clerkStatus]); - console.log('queryStatus', queryStatus, queryClient); + // console.log('queryStatus', queryStatus, queryClient); return ( // @ts-expect-error value passed is of type IsomorphicClerk where the context expects LoadedClerk @@ -113,7 +113,7 @@ export function ClerkContextProvider(props: ClerkContextProvider) { diff --git a/packages/shared/src/react/contexts.tsx b/packages/shared/src/react/contexts.tsx index 0a863e232c0..12582bc6236 100644 --- a/packages/shared/src/react/contexts.tsx +++ b/packages/shared/src/react/contexts.tsx @@ -10,7 +10,6 @@ import type { SignedInSessionResource, UserResource, } from '@clerk/types'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { PropsWithChildren } from 'react'; import React from 'react'; @@ -60,27 +59,24 @@ const OrganizationProvider = ({ children, organization, swrConfig, - queryClient, + // queryClient, }: PropsWithChildren< OrganizationContextProps & { // Exporting inferred types directly from SWR will result in error while building declarations swrConfig?: any; - queryClient?: QueryClient; + // queryClient?: QueryClient; } >) => { - const [defaultClient] = React.useState(() => new QueryClient()); return ( - - - - {children} - - - + + + {children} + + ); }; diff --git a/packages/shared/src/react/hooks/useSubscription.tsx b/packages/shared/src/react/hooks/useSubscription.tsx index 76bce1fbee5..3e2fa3c6fee 100644 --- a/packages/shared/src/react/hooks/useSubscription.tsx +++ b/packages/shared/src/react/hooks/useSubscription.tsx @@ -1,6 +1,6 @@ import type { EnvironmentResource, ForPayerType } from '@clerk/types'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { useCallback, useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { eventMethodCalled } from '../../telemetry/events'; import { useSWR } from '../clerk-swr'; @@ -80,6 +80,26 @@ export const useSubscriptionPrev = (params?: UseSubscriptionParams) => { }; }; +const useClerkQueryClient = () => { + const clerk = useClerkInstanceContext(); + const [queryStatus, setQueryStatus] = useState('loading'); + useEffect(() => { + // @ts-expect-error - queryClientStatus is not typed + clerk.on('queryClientStatus', setQueryStatus); + return () => { + // @ts-expect-error - queryClientStatus is not typed + clerk.off('queryClientStatus', setQueryStatus); + }; + }, [clerk]); + + const queryClient = useMemo(() => { + // @ts-expect-error - __internal_queryClient is not typed + return clerk.__internal_queryClient; + }, [queryStatus, clerk.status]); + + return queryClient; +}; + export const useSubscription = (params?: UseSubscriptionParams) => { useAssertWrappedByClerkProvider(hookName); @@ -99,7 +119,9 @@ export const useSubscription = (params?: UseSubscriptionParams) => { ? environment?.commerceSettings.billing.organization.enabled : environment?.commerceSettings.billing.user.enabled; - const queryClient = useQueryClient(); + const queryClient = useClerkQueryClient(); + // console.log('useInfiniteQuery', useInfiniteQuery); + // console.log('useInfiniteQuery', useMutation); const queryKey = useMemo(() => { return [ @@ -111,20 +133,23 @@ export const useSubscription = (params?: UseSubscriptionParams) => { ]; }, [user?.id, isOrganization, organization?.id]); - const query = useQuery({ - queryKey, - queryFn: ({ queryKey }) => { - const obj = queryKey[1] as { - args: { - orgId?: string; + const query = useQuery( + { + queryKey, + queryFn: ({ queryKey }) => { + const obj = queryKey[1] as { + args: { + orgId?: string; + }; }; - }; - return clerk.billing.getSubscription(obj.args); + return clerk.billing.getSubscription(obj.args); + }, + staleTime: 1_0000 * 60, + enabled: Boolean(user?.id && billingEnabled) && clerk.status === 'ready', + // placeholderData }, - staleTime: 1_0000 * 60, - enabled: Boolean(user?.id && billingEnabled) && clerk.status === 'ready', - // placeholderData - }); + queryClient, + ); const revalidate = useCallback(() => queryClient.invalidateQueries({ queryKey }), [queryClient, queryKey]); From e6d65617922d362cf4494acbd4b60afa44da8414 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 3 Oct 2025 17:08:20 +0300 Subject: [PATCH 3/8] update lock file --- pnpm-lock.yaml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cce2c25ef42..2dd4f21f87c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -470,6 +470,9 @@ importers: '@swc/helpers': specifier: ^0.5.17 version: 0.5.17 + '@tanstack/query-core': + specifier: ^5.87.4 + version: 5.90.2 '@zxcvbn-ts/core': specifier: 3.0.4 version: 3.0.4 @@ -922,6 +925,9 @@ importers: '@clerk/types': specifier: workspace:^ version: link:../types + '@tanstack/react-query': + specifier: ^5.87.4 + version: 5.90.2(react@18.3.1) dequal: specifier: 2.0.3 version: 2.0.3 @@ -2760,7 +2766,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {node: '>=0.10.0'} + engines: {'0': node >=0.10.0} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} @@ -4883,6 +4889,14 @@ packages: resolution: {integrity: sha512-cs1WKawpXIe+vSTeiZUuSBy8JFjEuDgdMKZFRLKwQysKo8y2q6Q1HvS74Yw+m5IhOW1nTZooa6rlgdfXcgFAaw==} engines: {node: '>=12'} + '@tanstack/query-core@5.90.2': + resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} + + '@tanstack/react-query@5.90.2': + resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} + peerDependencies: + react: ^18 || ^19 + '@tanstack/react-router@1.131.49': resolution: {integrity: sha512-WHgWJ053W8VU8lUYh8abSHVPeQdpaCpfaUAbV+3uYXbip2G+qlmI/Gsbh/BBV3bYtIi6l3t5dqx3ffCXNTzB5Q==} engines: {node: '>=12'} @@ -19649,6 +19663,13 @@ snapshots: '@tanstack/history@1.131.2': {} + '@tanstack/query-core@5.90.2': {} + + '@tanstack/react-query@5.90.2(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.90.2 + react: 18.3.1 + '@tanstack/react-router@1.131.49(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/history': 1.131.2 From 82901adc323060fe0c22c4ae2b84cc089aaab215 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 3 Oct 2025 18:53:30 +0300 Subject: [PATCH 4/8] wip --- packages/clerk-js/src/core/clerk.ts | 17 +- .../src/contexts/ClerkContextProvider.tsx | 33 ++-- packages/shared/global.d.ts | 8 + packages/shared/src/react/contexts.tsx | 6 +- .../src/react/hooks/useSubscription.rq.tsx | 132 ++++++++++++++ .../src/react/hooks/useSubscription.swr.tsx | 68 ++++++++ .../src/react/hooks/useSubscription.tsx | 164 +----------------- .../src/react/hooks/useSubscription.types.ts | 21 +++ .../react/providers/DataClientProvider.rq.tsx | 11 ++ .../providers/DataClientProvider.swr.tsx | 9 + .../react/providers/DataClientProvider.tsx | 1 + .../shared/src/types/virtual-data-hooks.d.ts | 7 + packages/shared/tsconfig.json | 7 +- packages/shared/tsup.config.ts | 41 ++++- 14 files changed, 335 insertions(+), 190 deletions(-) create mode 100644 packages/shared/src/react/hooks/useSubscription.rq.tsx create mode 100644 packages/shared/src/react/hooks/useSubscription.swr.tsx create mode 100644 packages/shared/src/react/hooks/useSubscription.types.ts create mode 100644 packages/shared/src/react/providers/DataClientProvider.rq.tsx create mode 100644 packages/shared/src/react/providers/DataClientProvider.swr.tsx create mode 100644 packages/shared/src/react/providers/DataClientProvider.tsx create mode 100644 packages/shared/src/types/virtual-data-hooks.d.ts diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index a1b65779b5f..98fadc8b56f 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -95,7 +95,7 @@ import type { WaitlistResource, Web3Provider, } from '@clerk/types'; -import type { QueryClient } from '@tanstack/query-core'; +import { QueryClient } from '@tanstack/query-core'; import { debugLogger, initDebugLogger } from '@/utils/debug'; @@ -223,7 +223,7 @@ export class Clerk implements ClerkInterface { // converted to protected environment to support `updateEnvironment` type assertion protected environment?: EnvironmentResource | null; - #queryClient: QueryClient | undefined; + #queryClient: QueryClient | undefined = new QueryClient(); #publishableKey = ''; #domain: DomainOrProxyUrl['domain']; #proxyUrl: DomainOrProxyUrl['proxyUrl']; @@ -242,14 +242,19 @@ export class Clerk implements ClerkInterface { #touchThrottledUntil = 0; #publicEventBus = createClerkEventBus(); - get __internal_queryClient(): QueryClient | undefined { - return this.#queryClient; + get __internal_queryClient(): { __tag: 'clerk-rq-client'; client: QueryClient } | undefined { + return this.#queryClient + ? { + __tag: 'clerk-rq-client', // make this a symbol + client: this.#queryClient, + } + : undefined; } public async getInternalQueryClient(): Promise { - const QueryClient = await import('./query-core').then(module => module.QueryClient); + // const QueryClient = await import('./query-core').then(module => module.QueryClient); if (!this.#queryClient) { - this.#queryClient = new QueryClient(); + // this.#queryClient = new QueryClient(); // @ts-expect-error - queryClientStatus is not typed this.#publicEventBus.emit('queryClientStatus', 'ready'); } diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index 3852f35ad01..3679b7dca96 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -88,20 +88,23 @@ export function ClerkContextProvider(props: ClerkContextProvider) { return { value }; }, [orgId, organization]); - const [queryStatus, setQueryStatus] = React.useState('loading'); - - React.useEffect(() => { - // @ts-expect-error - queryClientStatus is not typed - clerk.on('queryClientStatus', setQueryStatus); - return () => { - // @ts-expect-error - queryClientStatus is not typed - clerk.off('queryClientStatus', setQueryStatus); - }; - }, [clerk]); - - const queryClient = React.useMemo(() => { - return clerk.__internal_queryClient; - }, [queryStatus, clerkStatus]); + // const [queryStatus, setQueryStatus] = React.useState('loading'); + + // React.useEffect(() => { + // // @ts-expect-error - queryClientStatus is not typed + // clerk.on('queryClientStatus', (e)=>{ + // console.log('on queryClientStatus', e); + // setQueryStatus(e); + // }); + // return () => { + // // @ts-expect-error - queryClientStatus is not typed + // clerk.off('queryClientStatus', setQueryStatus); + // }; + // }, [clerk]); + + // const queryClient = React.useMemo(() => { + // return clerk.__internal_queryClient; + // }, [queryStatus, clerkStatus]); // console.log('queryStatus', queryStatus, queryClient); @@ -111,7 +114,7 @@ export function ClerkContextProvider(props: ClerkContextProvider) { diff --git a/packages/shared/global.d.ts b/packages/shared/global.d.ts index 5776b61ae17..22af55556c5 100644 --- a/packages/shared/global.d.ts +++ b/packages/shared/global.d.ts @@ -10,3 +10,11 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv; } + +declare module 'virtual:data-hooks/*' { + // Generic export signatures to satisfy type resolution for virtual modules + export const DataClientProvider: any; + export const useSubscription: any; + const mod: any; + export default mod; +} diff --git a/packages/shared/src/react/contexts.tsx b/packages/shared/src/react/contexts.tsx index 12582bc6236..eb9bbdb6b2e 100644 --- a/packages/shared/src/react/contexts.tsx +++ b/packages/shared/src/react/contexts.tsx @@ -13,8 +13,8 @@ import type { import type { PropsWithChildren } from 'react'; import React from 'react'; -import { SWRConfig } from './clerk-swr'; import { createContextAndHook } from './hooks/createContextAndHook'; +import { DataClientProvider } from './providers/DataClientProvider'; const [ClerkInstanceContext, useClerkInstanceContext] = createContextAndHook('ClerkInstanceContext'); const [UserContext, useUserContext] = createContextAndHook('UserContext'); @@ -68,7 +68,7 @@ const OrganizationProvider = ({ } >) => { return ( - + {children} - + ); }; diff --git a/packages/shared/src/react/hooks/useSubscription.rq.tsx b/packages/shared/src/react/hooks/useSubscription.rq.tsx new file mode 100644 index 00000000000..a1dcb2c4913 --- /dev/null +++ b/packages/shared/src/react/hooks/useSubscription.rq.tsx @@ -0,0 +1,132 @@ +import type { BillingSubscriptionResource, EnvironmentResource } from '@clerk/types'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { eventMethodCalled } from '../../telemetry/events'; +import { + useAssertWrappedByClerkProvider, + useClerkInstanceContext, + useOrganizationContext, + useUserContext, +} from '../contexts'; +import type { SubscriptionResult, UseSubscriptionParams } from './useSubscription.types'; + +const hookName = 'useSubscription'; + +/** + * @internal + */ +export function useDebounce(value: T, delay: number): T { + const [throttledValue, setThrottledValue] = useState(value); + const lastUpdated = useRef(null); + + useEffect(() => { + const now = Date.now(); + + if (lastUpdated.current && now >= lastUpdated.current + delay) { + lastUpdated.current = now; + setThrottledValue(value); + } else { + const id = window.setTimeout(() => { + lastUpdated.current = now; + setThrottledValue(value); + }, delay); + + return () => window.clearTimeout(id); + } + }, [value, delay]); + + return throttledValue; +} + +const useClerkQueryClient = () => { + const clerk = useClerkInstanceContext(); + // // @ts-expect-error - __internal_queryClient is not typed + // console.log('useClerkQueryClient, clerk', clerk.__internal_queryClient); + // @ts-expect-error - __internal_queryClient is not typed + const [queryStatus, setQueryStatus] = useState('__tag' in clerk.__internal_queryClient ? 'ready' : 'loading'); + console.log('useClerkQueryClient, queryStatus', queryStatus); + useEffect(() => { + // @ts-expect-error - queryClientStatus is not typed + clerk.on('queryClientStatus', setQueryStatus); + return () => { + // @ts-expect-error - queryClientStatus is not typed + clerk.off('queryClientStatus', setQueryStatus); + }; + }, [clerk]); + + const queryClient = useMemo(() => { + // @ts-expect-error - __internal_queryClient is not typed + console.log('useClerkQueryClient, clerk.__internal_queryClient', clerk.__internal_queryClient); + // @ts-expect-error - __internal_queryClient is not typed + return clerk.__internal_queryClient; + // @ts-expect-error - __internal_queryClient is not typed + }, [queryStatus, clerk.status, clerk.__internal_queryClient]); + + const debouncedQueryStatus = useDebounce( + '__tag' in queryClient && queryClient.__tag === 'clerk-rq-client' ? 'ready' : queryStatus, + 5_000, + ); + console.log('useClerkQueryClient, debouncedQueryStatus', debouncedQueryStatus); + + return [queryClient.client, debouncedQueryStatus]; +}; + +/** + * + */ +export function useSubscription(params?: UseSubscriptionParams): SubscriptionResult { + useAssertWrappedByClerkProvider(hookName); + + const clerk = useClerkInstanceContext(); + const user = useUserContext(); + const { organization } = useOrganizationContext(); + + // @ts-expect-error `__unstable__environment` is not typed + const environment = clerk.__unstable__environment as unknown as EnvironmentResource | null | undefined; + + clerk.telemetry?.record(eventMethodCalled(hookName)); + + const isOrganization = params?.for === 'organization'; + const billingEnabled = isOrganization + ? environment?.commerceSettings.billing.organization.enabled + : environment?.commerceSettings.billing.user.enabled; + + const [queryClient, queryStatus] = useClerkQueryClient(); + + const queryKey = useMemo(() => { + return [ + 'commerce-subscription', + { + userId: user?.id, + args: { orgId: isOrganization ? organization?.id : undefined }, + }, + ]; + }, [user?.id, isOrganization, organization?.id]); + + console.log('enabled', Boolean(user?.id && billingEnabled) && clerk.status === 'ready' && queryStatus === 'ready'); + + const query = useQuery( + { + queryKey, + queryFn: ({ queryKey }) => { + const obj = queryKey[1] as { args: { orgId?: string } }; + console.log('queryFn, obj', obj); + return clerk.billing.getSubscription(obj.args); + }, + staleTime: 1_000 * 60, + enabled: Boolean(user?.id && billingEnabled) && clerk.status === 'ready' && queryStatus === 'ready', + }, + queryClient, + ); + + const revalidate = useCallback(() => queryClient.invalidateQueries({ queryKey }), [queryClient, queryKey]); + + return { + data: query.data, + error: query.error, + isLoading: query.isLoading, + isFetching: query.isFetching, + revalidate, + }; +} diff --git a/packages/shared/src/react/hooks/useSubscription.swr.tsx b/packages/shared/src/react/hooks/useSubscription.swr.tsx new file mode 100644 index 00000000000..2261e5cba2c --- /dev/null +++ b/packages/shared/src/react/hooks/useSubscription.swr.tsx @@ -0,0 +1,68 @@ +import type { BillingSubscriptionResource, EnvironmentResource } from '@clerk/types'; +import { useCallback } from 'react'; + +import { eventMethodCalled } from '../../telemetry/events'; +import { useSWR } from '../clerk-swr'; +import { + useAssertWrappedByClerkProvider, + useClerkInstanceContext, + useOrganizationContext, + useUserContext, +} from '../contexts'; +import type { SubscriptionResult, UseSubscriptionParams } from './useSubscription.types'; + +const hookName = 'useSubscription'; + +/** + * @internal + */ +export function useSubscription(params?: UseSubscriptionParams): SubscriptionResult { + useAssertWrappedByClerkProvider(hookName); + console.log('useSubscription SWR'); + + const clerk = useClerkInstanceContext(); + const user = useUserContext(); + const { organization } = useOrganizationContext(); + + // @ts-expect-error `__unstable__environment` is not typed + const environment = clerk.__unstable__environment as unknown as EnvironmentResource | null | undefined; + + clerk.telemetry?.record(eventMethodCalled(hookName)); + + const isOrganization = params?.for === 'organization'; + const billingEnabled = isOrganization + ? environment?.commerceSettings.billing.organization.enabled + : environment?.commerceSettings.billing.user.enabled; + + const swr = useSWR( + billingEnabled + ? { + type: 'commerce-subscription', + userId: user?.id, + args: { orgId: isOrganization ? organization?.id : undefined }, + } + : null, + ({ args, userId }) => { + if (userId) { + return clerk.billing.getSubscription(args); + } + return null; + }, + { + dedupingInterval: 1_000 * 60, + keepPreviousData: params?.keepPreviousData, + }, + ); + + const revalidate = useCallback(() => { + void swr.mutate(); + }, [swr]); + + return { + data: swr.data, + error: swr.error, + isLoading: swr.isLoading, + isFetching: swr.isValidating, + revalidate, + }; +} diff --git a/packages/shared/src/react/hooks/useSubscription.tsx b/packages/shared/src/react/hooks/useSubscription.tsx index 3e2fa3c6fee..98cd031a355 100644 --- a/packages/shared/src/react/hooks/useSubscription.tsx +++ b/packages/shared/src/react/hooks/useSubscription.tsx @@ -1,163 +1 @@ -import type { EnvironmentResource, ForPayerType } from '@clerk/types'; -import { useQuery } from '@tanstack/react-query'; -import { useCallback, useEffect, useMemo, useState } from 'react'; - -import { eventMethodCalled } from '../../telemetry/events'; -import { useSWR } from '../clerk-swr'; -import { - useAssertWrappedByClerkProvider, - useClerkInstanceContext, - useOrganizationContext, - useUserContext, -} from '../contexts'; - -const hookName = 'useSubscription'; - -type UseSubscriptionParams = { - for?: ForPayerType; - /** - * If `true`, the previous data will be kept in the cache until new data is fetched. - * - * @default false - */ - keepPreviousData?: boolean; -}; - -/** - * @internal - * - * Fetches subscription data for the current user or organization. - * - * @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change. It is advised to [pin](https://clerk.com/docs/pinning) the SDK version and the clerk-js version to avoid breaking changes. - */ -export const useSubscriptionPrev = (params?: UseSubscriptionParams) => { - useAssertWrappedByClerkProvider(hookName); - - const clerk = useClerkInstanceContext(); - const user = useUserContext(); - const { organization } = useOrganizationContext(); - - // @ts-expect-error `__unstable__environment` is not typed - const environment = clerk.__unstable__environment as unknown as EnvironmentResource | null | undefined; - - clerk.telemetry?.record(eventMethodCalled(hookName)); - - const isOrganization = params?.for === 'organization'; - const billingEnabled = isOrganization - ? environment?.commerceSettings.billing.organization.enabled - : environment?.commerceSettings.billing.user.enabled; - - const swr = useSWR( - billingEnabled - ? { - type: 'commerce-subscription', - userId: user?.id, - args: { orgId: isOrganization ? organization?.id : undefined }, - } - : null, - ({ args, userId }) => { - // This allows for supporting keeping previous data between revalidations - // but also hides the stale data on sign-out. - if (userId) { - return clerk.billing.getSubscription(args); - } - return null; - }, - { - dedupingInterval: 1_000 * 60, - keepPreviousData: params?.keepPreviousData, - }, - ); - - const revalidate = useCallback(() => swr.mutate(), [swr.mutate]); - - return { - data: swr.data, - error: swr.error, - isLoading: swr.isLoading, - isFetching: swr.isValidating, - revalidate, - }; -}; - -const useClerkQueryClient = () => { - const clerk = useClerkInstanceContext(); - const [queryStatus, setQueryStatus] = useState('loading'); - useEffect(() => { - // @ts-expect-error - queryClientStatus is not typed - clerk.on('queryClientStatus', setQueryStatus); - return () => { - // @ts-expect-error - queryClientStatus is not typed - clerk.off('queryClientStatus', setQueryStatus); - }; - }, [clerk]); - - const queryClient = useMemo(() => { - // @ts-expect-error - __internal_queryClient is not typed - return clerk.__internal_queryClient; - }, [queryStatus, clerk.status]); - - return queryClient; -}; - -export const useSubscription = (params?: UseSubscriptionParams) => { - useAssertWrappedByClerkProvider(hookName); - - const clerk = useClerkInstanceContext(); - const user = useUserContext(); - const { organization } = useOrganizationContext(); - - // console.log('cache', cache); - - // @ts-expect-error `__unstable__environment` is not typed - const environment = clerk.__unstable__environment as unknown as EnvironmentResource | null | undefined; - - clerk.telemetry?.record(eventMethodCalled(hookName)); - - const isOrganization = params?.for === 'organization'; - const billingEnabled = isOrganization - ? environment?.commerceSettings.billing.organization.enabled - : environment?.commerceSettings.billing.user.enabled; - - const queryClient = useClerkQueryClient(); - // console.log('useInfiniteQuery', useInfiniteQuery); - // console.log('useInfiniteQuery', useMutation); - - const queryKey = useMemo(() => { - return [ - 'commerce-subscription', - { - userId: user?.id, - args: { orgId: isOrganization ? organization?.id : undefined }, - }, - ]; - }, [user?.id, isOrganization, organization?.id]); - - const query = useQuery( - { - queryKey, - queryFn: ({ queryKey }) => { - const obj = queryKey[1] as { - args: { - orgId?: string; - }; - }; - return clerk.billing.getSubscription(obj.args); - }, - staleTime: 1_0000 * 60, - enabled: Boolean(user?.id && billingEnabled) && clerk.status === 'ready', - // placeholderData - }, - queryClient, - ); - - const revalidate = useCallback(() => queryClient.invalidateQueries({ queryKey }), [queryClient, queryKey]); - - return { - data: query.data, - error: query.error, - isLoading: query.isLoading, - isFetching: query.isFetching, - revalidate, - }; -}; +export { useSubscription } from 'virtual:data-hooks/useSubscription'; diff --git a/packages/shared/src/react/hooks/useSubscription.types.ts b/packages/shared/src/react/hooks/useSubscription.types.ts new file mode 100644 index 00000000000..9835de9857b --- /dev/null +++ b/packages/shared/src/react/hooks/useSubscription.types.ts @@ -0,0 +1,21 @@ +import type { ForPayerType } from '@clerk/types'; + +export type UseSubscriptionParams = { + for?: ForPayerType; + /** + * If true, the previous data will be kept in the cache until new data is fetched. + * Defaults to false. + */ + keepPreviousData?: boolean; +}; + +export type SubscriptionResult = { + data: TData | undefined | null; + error: unknown; + isLoading: boolean; + isFetching: boolean; + /** + * Revalidate or refetch the subscription data. + */ + revalidate: () => Promise | void; +}; diff --git a/packages/shared/src/react/providers/DataClientProvider.rq.tsx b/packages/shared/src/react/providers/DataClientProvider.rq.tsx new file mode 100644 index 00000000000..54d61125af6 --- /dev/null +++ b/packages/shared/src/react/providers/DataClientProvider.rq.tsx @@ -0,0 +1,11 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import React, { type PropsWithChildren } from 'react'; + +const queryClient = new QueryClient(); + +/** + * + */ +export function DataClientProvider({ children }: PropsWithChildren<{}>) { + return {children}; +} diff --git a/packages/shared/src/react/providers/DataClientProvider.swr.tsx b/packages/shared/src/react/providers/DataClientProvider.swr.tsx new file mode 100644 index 00000000000..2f781d61907 --- /dev/null +++ b/packages/shared/src/react/providers/DataClientProvider.swr.tsx @@ -0,0 +1,9 @@ +import React, { type PropsWithChildren } from 'react'; +import { SWRConfig } from 'swr'; + +/** + * + */ +export function DataClientProvider({ children }: PropsWithChildren<{}>) { + return {children}; +} diff --git a/packages/shared/src/react/providers/DataClientProvider.tsx b/packages/shared/src/react/providers/DataClientProvider.tsx new file mode 100644 index 00000000000..c12465f1a6b --- /dev/null +++ b/packages/shared/src/react/providers/DataClientProvider.tsx @@ -0,0 +1 @@ +export { DataClientProvider } from 'virtual:data-hooks/DataClientProvider'; diff --git a/packages/shared/src/types/virtual-data-hooks.d.ts b/packages/shared/src/types/virtual-data-hooks.d.ts new file mode 100644 index 00000000000..4e27d9f888c --- /dev/null +++ b/packages/shared/src/types/virtual-data-hooks.d.ts @@ -0,0 +1,7 @@ +declare module 'virtual:data-hooks/*' { + // Generic export signatures to satisfy type resolution for virtual modules + export const DataClientProvider: any; + export const useSubscription: any; + const mod: any; + export default mod; +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index 05919363e30..91567832fc9 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -13,8 +13,11 @@ "resolveJsonModule": true, "jsx": "react", "lib": ["ES2022", "DOM", "WebWorker"], - "allowJs": true + "allowJs": true, + "paths": { + "virtual:data-hooks/*": ["./src/react/hooks/useSubscription.swr.tsx"] + } }, "exclude": ["node_modules"], - "include": ["src", "global.d.ts"] + "include": ["src", "global.d.ts", "src/types/virtual-data-hooks.d.ts"] } diff --git a/packages/shared/tsup.config.ts b/packages/shared/tsup.config.ts index 9d5b8e121c9..ba115e8f9b1 100644 --- a/packages/shared/tsup.config.ts +++ b/packages/shared/tsup.config.ts @@ -1,9 +1,13 @@ import type { Plugin } from 'esbuild'; import * as esbuild from 'esbuild'; +import * as fs from 'fs'; import { readFile } from 'fs/promises'; +import * as path from 'path'; import { defineConfig } from 'tsup'; +// @ts-ignore - resolved by tsup build (resolveJsonModule not needed at type time) import { version as clerkJsVersion } from '../clerk-js/package.json'; +// @ts-ignore - resolved by tsup build import { name, version } from './package.json'; export default defineConfig(overrideOptions => { @@ -26,12 +30,13 @@ export default defineConfig(overrideOptions => { dts: true, target: 'es2022', external: ['react', 'react-dom'], - esbuildPlugins: [WebWorkerMinifyPlugin as any], + esbuildPlugins: [WebWorkerMinifyPlugin as any, HookAliasPlugin() as any], define: { PACKAGE_NAME: `"${name}"`, PACKAGE_VERSION: `"${version}"`, JS_PACKAGE_VERSION: `"${clerkJsVersion}"`, __DEV__: `${isWatch}`, + __USE_RQ__: JSON.stringify(process.env.CLERK_USE_RQ === 'true'), }, }; }); @@ -49,3 +54,37 @@ export const WebWorkerMinifyPlugin: Plugin = { }); }, }; + +const HookAliasPlugin = (): Plugin => { + const useRQ = process.env.CLERK_USE_RQ === 'true'; + const rqHooks = new Set((process.env.CLERK_RQ_HOOKS ?? '').split(',').filter(Boolean)); + const baseDir = __dirname; // packages/shared + + const resolveImpl = (specifier: string) => { + const name = specifier.replace('virtual:data-hooks/', ''); + const chosenRQ = rqHooks.has(name) || useRQ; + const impl = chosenRQ ? `${name}.rq.tsx` : `${name}.swr.tsx`; + + const candidates = name.toLowerCase().includes('provider') + ? [path.join(baseDir, 'src', 'react', 'providers', impl), path.join(baseDir, 'src', 'react', 'hooks', impl)] + : [path.join(baseDir, 'src', 'react', 'hooks', impl), path.join(baseDir, 'src', 'react', 'providers', impl)]; + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + // default to first candidate; esbuild will emit a clear error if missing + return candidates[0]; + }; + + return { + name: 'hook-alias-plugin', + setup(build) { + build.onResolve({ filter: /^virtual:data-hooks\// }, args => { + const resolved = resolveImpl(args.path); + return { path: resolved }; + }); + }, + }; +}; From f5c27e3af2c46ed1ed230045c3e8f69843b2a95b Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 3 Oct 2025 21:14:55 +0300 Subject: [PATCH 5/8] feat(shared): `useSubscription` variant with React Query --- packages/clerk-js/rspack.config.js | 6 ++ packages/clerk-js/src/core/clerk.ts | 27 +++--- .../src/contexts/ClerkContextProvider.tsx | 20 ----- packages/react/src/isomorphicClerk.ts | 78 ++-------------- packages/shared/package.json | 2 +- .../shared/src/react/clerk-rq/queryOptions.ts | 80 +++++++++++++++++ packages/shared/src/react/clerk-rq/types.ts | 53 +++++++++++ .../react/clerk-rq/use-clerk-query-client.ts | 82 +++++++++++++++++ .../shared/src/react/clerk-rq/useBaseQuery.ts | 55 ++++++++++++ .../shared/src/react/clerk-rq/useQuery.ts | 37 ++++++++ packages/shared/src/react/contexts.tsx | 8 +- .../src/react/hooks/useSubscription.rq.tsx | 90 +++---------------- .../src/react/hooks/useSubscription.swr.tsx | 3 +- .../react/providers/DataClientProvider.rq.tsx | 11 --- .../providers/DataClientProvider.swr.tsx | 9 -- .../react/providers/DataClientProvider.tsx | 1 - .../react/providers/SWRConfigCompat.rq.tsx | 8 ++ .../react/providers/SWRConfigCompat.swr.tsx | 9 ++ .../src/react/providers/SWRConfigCompat.tsx | 1 + .../shared/src/types/virtual-data-hooks.d.ts | 2 +- packages/shared/tsconfig.json | 3 +- pnpm-lock.yaml | 26 ++---- 22 files changed, 382 insertions(+), 229 deletions(-) create mode 100644 packages/shared/src/react/clerk-rq/queryOptions.ts create mode 100644 packages/shared/src/react/clerk-rq/types.ts create mode 100644 packages/shared/src/react/clerk-rq/use-clerk-query-client.ts create mode 100644 packages/shared/src/react/clerk-rq/useBaseQuery.ts create mode 100644 packages/shared/src/react/clerk-rq/useQuery.ts delete mode 100644 packages/shared/src/react/providers/DataClientProvider.rq.tsx delete mode 100644 packages/shared/src/react/providers/DataClientProvider.swr.tsx delete mode 100644 packages/shared/src/react/providers/DataClientProvider.tsx create mode 100644 packages/shared/src/react/providers/SWRConfigCompat.rq.tsx create mode 100644 packages/shared/src/react/providers/SWRConfigCompat.swr.tsx create mode 100644 packages/shared/src/react/providers/SWRConfigCompat.tsx diff --git a/packages/clerk-js/rspack.config.js b/packages/clerk-js/rspack.config.js index 78467f67a79..552574af9f6 100644 --- a/packages/clerk-js/rspack.config.js +++ b/packages/clerk-js/rspack.config.js @@ -114,6 +114,12 @@ const common = ({ mode, variant, disableRHC = false }) => { chunks: 'all', enforce: true, }, + queryCoreVendor: { + test: /[\\/]node_modules[\\/](@tanstack\/query-core)[\\/]/, + name: 'query-core-vendors', + chunks: 'all', + enforce: true, + }, /** * Sign up is shared between the SignUp component and the SignIn component. */ diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 98fadc8b56f..892d5effcd3 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -95,7 +95,7 @@ import type { WaitlistResource, Web3Provider, } from '@clerk/types'; -import { QueryClient } from '@tanstack/query-core'; +import type { QueryClient } from '@tanstack/query-core'; import { debugLogger, initDebugLogger } from '@/utils/debug'; @@ -223,7 +223,7 @@ export class Clerk implements ClerkInterface { // converted to protected environment to support `updateEnvironment` type assertion protected environment?: EnvironmentResource | null; - #queryClient: QueryClient | undefined = new QueryClient(); + #queryClient: QueryClient | undefined; #publishableKey = ''; #domain: DomainOrProxyUrl['domain']; #proxyUrl: DomainOrProxyUrl['proxyUrl']; @@ -243,6 +243,19 @@ export class Clerk implements ClerkInterface { #publicEventBus = createClerkEventBus(); get __internal_queryClient(): { __tag: 'clerk-rq-client'; client: QueryClient } | undefined { + if (!this.#queryClient) { + void import('./query-core') + .then(module => module.QueryClient) + .then(QueryClient => { + if (this.#queryClient) { + return; + } + this.#queryClient = new QueryClient(); + // @ts-expect-error - queryClientStatus is not typed + this.#publicEventBus.emit('queryClientStatus', 'ready'); + }); + } + return this.#queryClient ? { __tag: 'clerk-rq-client', // make this a symbol @@ -251,16 +264,6 @@ export class Clerk implements ClerkInterface { : undefined; } - public async getInternalQueryClient(): Promise { - // const QueryClient = await import('./query-core').then(module => module.QueryClient); - if (!this.#queryClient) { - // this.#queryClient = new QueryClient(); - // @ts-expect-error - queryClientStatus is not typed - this.#publicEventBus.emit('queryClientStatus', 'ready'); - } - return this.#queryClient; - } - public __internal_getCachedResources: | (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>) | undefined; diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index 3679b7dca96..41abf3e4eb6 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -88,26 +88,6 @@ export function ClerkContextProvider(props: ClerkContextProvider) { return { value }; }, [orgId, organization]); - // const [queryStatus, setQueryStatus] = React.useState('loading'); - - // React.useEffect(() => { - // // @ts-expect-error - queryClientStatus is not typed - // clerk.on('queryClientStatus', (e)=>{ - // console.log('on queryClientStatus', e); - // setQueryStatus(e); - // }); - // return () => { - // // @ts-expect-error - queryClientStatus is not typed - // clerk.off('queryClientStatus', setQueryStatus); - // }; - // }, [clerk]); - - // const queryClient = React.useMemo(() => { - // return clerk.__internal_queryClient; - // }, [queryStatus, clerkStatus]); - - // console.log('queryStatus', queryStatus, queryClient); - return ( // @ts-expect-error value passed is of type IsomorphicClerk where the context expects LoadedClerk diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 4d97a3a2566..32ff8cc5529 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -115,66 +115,6 @@ type IsomorphicLoadedClerk = Without< apiKeys: APIKeysNamespace | undefined; }; -export type RecursiveMock = { - (...args: unknown[]): RecursiveMock; -} & { - readonly [key in string | symbol]: RecursiveMock; -}; - -/** - * Creates a recursively self-referential Proxy that safely handles: - * - Arbitrary property access (e.g., obj.any.prop.path) - * - Function calls at any level (e.g., obj.a().b.c()) - * - Construction (e.g., new obj.a.b()) - * - * Always returns itself to allow infinite chaining without throwing. - */ -function createRecursiveProxy(label: string = 'Mock'): RecursiveMock { - // The callable target for the proxy so that `apply` works - const callableTarget = function noop(): void {}; - - // eslint-disable-next-line prefer-const - let self: RecursiveMock; - const handler: ProxyHandler = { - get(_target, prop) { - // Avoid being treated as a Promise/thenable by test runners or frameworks - if (prop === 'then') { - return undefined; - } - if (prop === 'toString') { - return () => `[${label}]`; - } - if (prop === Symbol.toPrimitive) { - return () => 0; - } - return self; - }, - apply() { - return self; - }, - construct() { - return self as unknown as object; - }, - has() { - return true; - }, - set() { - return true; - }, - }; - - self = new Proxy(callableTarget, handler) as unknown as RecursiveMock; - return self; -} - -/** - * Returns a permissive mock compatible with `QueryClient` usage in tests. - * It accepts any chain of property accesses and calls without throwing. - */ -export function createMockQueryClient(): RecursiveMock { - return createRecursiveProxy('MockQueryClient') as unknown as RecursiveMock; -} - export class IsomorphicClerk implements IsomorphicLoadedClerk { private readonly mode: 'browser' | 'server'; private readonly options: IsomorphicClerkOptions; @@ -224,7 +164,6 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { #publishableKey: string; #eventBus = createClerkEventBus(); #stateProxy: StateProxy; - #__internal_queryClient = createMockQueryClient(); get publishableKey(): string { return this.#publishableKey; @@ -348,14 +287,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { get __internal_queryClient() { // @ts-expect-error - __internal_queryClient is not typed - if (!this.clerkjs?.__internal_queryClient) { - // @ts-expect-error - __internal_queryClient is not typed - void this.clerkjs?.getInternalQueryClient?.(); - this.prefetchQueryClientStatus = true; - } - - // @ts-expect-error - __internal_queryClient is not typed - return this.clerkjs?.__internal_queryClient || this.#__internal_queryClient; + return this.clerkjs?.__internal_queryClient; } get isSatellite() { @@ -693,10 +625,10 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { clerkjs.openWaitlist(this.preOpenWaitlist); } - if (this.prefetchQueryClientStatus) { - // @ts-expect-error - queryClientStatus is not typed - this.clerkjs.getInternalQueryClient?.(); - } + // if (this.prefetchQueryClientStatus) { + // // @ts-expect-error - queryClientStatus is not typed + // this.clerkjs.getInternalQueryClient?.(); + // } this.premountSignInNodes.forEach((props, node) => { clerkjs.mountSignIn(node, props); diff --git a/packages/shared/package.json b/packages/shared/package.json index a685a8abc1a..9c6f3a7a6e0 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -146,7 +146,6 @@ }, "dependencies": { "@clerk/types": "workspace:^", - "@tanstack/react-query": "^5.87.4", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", @@ -156,6 +155,7 @@ "devDependencies": { "@stripe/react-stripe-js": "3.1.1", "@stripe/stripe-js": "5.6.0", + "@tanstack/query-core": "5.87.4", "@types/glob-to-regexp": "0.4.4", "@types/js-cookie": "3.0.6", "cross-fetch": "^4.1.0", diff --git a/packages/shared/src/react/clerk-rq/queryOptions.ts b/packages/shared/src/react/clerk-rq/queryOptions.ts new file mode 100644 index 00000000000..ee92606d539 --- /dev/null +++ b/packages/shared/src/react/clerk-rq/queryOptions.ts @@ -0,0 +1,80 @@ +import type { + DataTag, + DefaultError, + InitialDataFunction, + NonUndefinedGuard, + OmitKeyof, + QueryFunction, + QueryKey, + SkipToken, +} from '@tanstack/query-core'; + +import type { UseQueryOptions } from './types'; + +export type UndefinedInitialDataOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = UseQueryOptions & { + initialData?: undefined | InitialDataFunction> | NonUndefinedGuard; +}; + +export type UnusedSkipTokenOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = OmitKeyof, 'queryFn'> & { + queryFn?: Exclude['queryFn'], SkipToken | undefined>; +}; + +export type DefinedInitialDataOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = Omit, 'queryFn'> & { + initialData: NonUndefinedGuard | (() => NonUndefinedGuard); + queryFn?: QueryFunction; +}; + +export function queryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: DefinedInitialDataOptions, +): DefinedInitialDataOptions & { + queryKey: DataTag; +}; + +export function queryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UnusedSkipTokenOptions, +): UnusedSkipTokenOptions & { + queryKey: DataTag; +}; + +export function queryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UndefinedInitialDataOptions, +): UndefinedInitialDataOptions & { + queryKey: DataTag; +}; + +/** + * + */ +export function queryOptions(options: unknown) { + return options; +} diff --git a/packages/shared/src/react/clerk-rq/types.ts b/packages/shared/src/react/clerk-rq/types.ts new file mode 100644 index 00000000000..2b524ab6fb0 --- /dev/null +++ b/packages/shared/src/react/clerk-rq/types.ts @@ -0,0 +1,53 @@ +import type { + DefaultError, + DefinedQueryObserverResult, + InfiniteQueryObserverOptions, + OmitKeyof, + QueryKey, + QueryObserverOptions, + QueryObserverResult, +} from '@tanstack/query-core'; + +export type AnyUseBaseQueryOptions = UseBaseQueryOptions; +export interface UseBaseQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> extends QueryObserverOptions { + /** + * Set this to `false` to unsubscribe this observer from updates to the query cache. + * Defaults to `true`. + */ + subscribed?: boolean; +} + +export type AnyUseQueryOptions = UseQueryOptions; +export interface UseQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> extends OmitKeyof, 'suspense'> {} + +export type AnyUseInfiniteQueryOptions = UseInfiniteQueryOptions; +export interface UseInfiniteQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +> extends OmitKeyof, 'suspense'> { + /** + * Set this to `false` to unsubscribe this observer from updates to the query cache. + * Defaults to `true`. + */ + subscribed?: boolean; +} + +export type UseBaseQueryResult = QueryObserverResult; + +export type UseQueryResult = UseBaseQueryResult; + +export type DefinedUseQueryResult = DefinedQueryObserverResult; diff --git a/packages/shared/src/react/clerk-rq/use-clerk-query-client.ts b/packages/shared/src/react/clerk-rq/use-clerk-query-client.ts new file mode 100644 index 00000000000..97d5f0bc360 --- /dev/null +++ b/packages/shared/src/react/clerk-rq/use-clerk-query-client.ts @@ -0,0 +1,82 @@ +import type { QueryClient } from '@tanstack/query-core'; +import { useEffect, useState } from 'react'; + +import { useClerkInstanceContext } from '../contexts'; + +export type RecursiveMock = { + (...args: unknown[]): RecursiveMock; +} & { + readonly [key in string | symbol]: RecursiveMock; +}; + +/** + * Creates a recursively self-referential Proxy that safely handles: + * - Arbitrary property access (e.g., obj.any.prop.path) + * - Function calls at any level (e.g., obj.a().b.c()) + * - Construction (e.g., new obj.a.b()) + * + * Always returns itself to allow infinite chaining without throwing. + */ +function createRecursiveProxy(label: string): RecursiveMock { + // The callable target for the proxy so that `apply` works + const callableTarget = function noop(): void {}; + + // eslint-disable-next-line prefer-const + let self: RecursiveMock; + const handler: ProxyHandler = { + get(_target, prop) { + // Avoid being treated as a Promise/thenable by test runners or frameworks + if (prop === 'then') { + return undefined; + } + if (prop === 'toString') { + return () => `[${label}]`; + } + if (prop === Symbol.toPrimitive) { + return () => 0; + } + return self; + }, + apply() { + return self; + }, + construct() { + return self as unknown as object; + }, + has() { + return false; + }, + set() { + return false; + }, + }; + + self = new Proxy(callableTarget, handler) as unknown as RecursiveMock; + return self; +} + +const mockQueryClient = createRecursiveProxy('ClerkMockQueryClient') as unknown as QueryClient; + +const useClerkQueryClient = (): QueryClient => { + const clerk = useClerkInstanceContext(); + + // @ts-expect-error - __internal_queryClient is not typed + const queryClient = clerk.__internal_queryClient as { __tag: 'clerk-rq-client'; client: QueryClient } | undefined; + const [, setQueryClientLoaded] = useState( + typeof queryClient === 'object' && '__tag' in queryClient && queryClient.__tag === 'clerk-rq-client', + ); + + useEffect(() => { + const _setQueryClientLoaded = () => setQueryClientLoaded(true); + // @ts-expect-error - queryClientStatus is not typed + clerk.on('queryClientStatus', _setQueryClientLoaded); + return () => { + // @ts-expect-error - queryClientStatus is not typed + clerk.off('queryClientStatus', _setQueryClientLoaded); + }; + }, [clerk, setQueryClientLoaded]); + + return queryClient?.client || mockQueryClient; +}; + +export { useClerkQueryClient }; diff --git a/packages/shared/src/react/clerk-rq/useBaseQuery.ts b/packages/shared/src/react/clerk-rq/useBaseQuery.ts new file mode 100644 index 00000000000..00613951acc --- /dev/null +++ b/packages/shared/src/react/clerk-rq/useBaseQuery.ts @@ -0,0 +1,55 @@ +/** + * Stripped down version of useBaseQuery from @tanstack/query-core. + * This implementation allows for an observer to be created every time a query client changes. + */ + +'use client'; +import type { QueryKey, QueryObserver, QueryObserverResult } from '@tanstack/query-core'; +import { noop, notifyManager } from '@tanstack/query-core'; +import * as React from 'react'; + +import type { UseBaseQueryOptions } from './types'; +import { useClerkQueryClient } from './use-clerk-query-client'; + +/** + * + */ +export function useBaseQuery( + options: UseBaseQueryOptions, + Observer: typeof QueryObserver, +): QueryObserverResult { + const client = useClerkQueryClient(); + const defaultedOptions = client.defaultQueryOptions(options); + + const observer = React.useMemo(() => { + return new Observer(client, defaultedOptions); + }, [client]); + + // note: this must be called before useSyncExternalStore + const result = observer.getOptimisticResult(defaultedOptions); + + const shouldSubscribe = options.subscribed !== false; + React.useSyncExternalStore( + React.useCallback( + onStoreChange => { + const unsubscribe = shouldSubscribe ? observer.subscribe(notifyManager.batchCalls(onStoreChange)) : noop; + + // Update result to make sure we did not miss any query updates + // between creating the observer and subscribing to it. + observer.updateResult(); + + return unsubscribe; + }, + [observer, shouldSubscribe], + ), + () => observer.getCurrentResult(), + () => observer.getCurrentResult(), + ); + + React.useEffect(() => { + observer.setOptions(defaultedOptions); + }, [defaultedOptions, observer]); + + // Handle result property usage tracking + return !defaultedOptions.notifyOnChangeProps ? observer.trackResult(result) : result; +} diff --git a/packages/shared/src/react/clerk-rq/useQuery.ts b/packages/shared/src/react/clerk-rq/useQuery.ts new file mode 100644 index 00000000000..33120a541ff --- /dev/null +++ b/packages/shared/src/react/clerk-rq/useQuery.ts @@ -0,0 +1,37 @@ +'use client'; +import type { DefaultError, NoInfer, QueryKey } from '@tanstack/query-core'; +import { QueryObserver } from '@tanstack/query-core'; + +import type { DefinedInitialDataOptions, UndefinedInitialDataOptions } from './queryOptions'; +import type { DefinedUseQueryResult, UseQueryOptions, UseQueryResult } from './types'; +import { useBaseQuery } from './useBaseQuery'; + +export function useClerkQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: DefinedInitialDataOptions, +): DefinedUseQueryResult, TError>; + +export function useClerkQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>(options: UndefinedInitialDataOptions): UseQueryResult, TError>; + +export function useClerkQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>(options: UseQueryOptions): UseQueryResult, TError>; + +/** + * + */ +export function useClerkQuery(options: UseQueryOptions) { + return useBaseQuery(options, QueryObserver); +} diff --git a/packages/shared/src/react/contexts.tsx b/packages/shared/src/react/contexts.tsx index eb9bbdb6b2e..aeca4229022 100644 --- a/packages/shared/src/react/contexts.tsx +++ b/packages/shared/src/react/contexts.tsx @@ -14,7 +14,7 @@ import type { PropsWithChildren } from 'react'; import React from 'react'; import { createContextAndHook } from './hooks/createContextAndHook'; -import { DataClientProvider } from './providers/DataClientProvider'; +import { SWRConfigCompat } from './providers/SWRConfigCompat'; const [ClerkInstanceContext, useClerkInstanceContext] = createContextAndHook('ClerkInstanceContext'); const [UserContext, useUserContext] = createContextAndHook('UserContext'); @@ -59,16 +59,14 @@ const OrganizationProvider = ({ children, organization, swrConfig, - // queryClient, }: PropsWithChildren< OrganizationContextProps & { // Exporting inferred types directly from SWR will result in error while building declarations swrConfig?: any; - // queryClient?: QueryClient; } >) => { return ( - + {children} - + ); }; diff --git a/packages/shared/src/react/hooks/useSubscription.rq.tsx b/packages/shared/src/react/hooks/useSubscription.rq.tsx index a1dcb2c4913..02e4ded4a1f 100644 --- a/packages/shared/src/react/hooks/useSubscription.rq.tsx +++ b/packages/shared/src/react/hooks/useSubscription.rq.tsx @@ -1,8 +1,9 @@ import type { BillingSubscriptionResource, EnvironmentResource } from '@clerk/types'; -import { useQuery } from '@tanstack/react-query'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { eventMethodCalled } from '../../telemetry/events'; +import { useClerkQueryClient } from '../clerk-rq/use-clerk-query-client'; +import { useClerkQuery } from '../clerk-rq/useQuery'; import { useAssertWrappedByClerkProvider, useClerkInstanceContext, @@ -15,65 +16,8 @@ const hookName = 'useSubscription'; /** * @internal - */ -export function useDebounce(value: T, delay: number): T { - const [throttledValue, setThrottledValue] = useState(value); - const lastUpdated = useRef(null); - - useEffect(() => { - const now = Date.now(); - - if (lastUpdated.current && now >= lastUpdated.current + delay) { - lastUpdated.current = now; - setThrottledValue(value); - } else { - const id = window.setTimeout(() => { - lastUpdated.current = now; - setThrottledValue(value); - }, delay); - - return () => window.clearTimeout(id); - } - }, [value, delay]); - - return throttledValue; -} - -const useClerkQueryClient = () => { - const clerk = useClerkInstanceContext(); - // // @ts-expect-error - __internal_queryClient is not typed - // console.log('useClerkQueryClient, clerk', clerk.__internal_queryClient); - // @ts-expect-error - __internal_queryClient is not typed - const [queryStatus, setQueryStatus] = useState('__tag' in clerk.__internal_queryClient ? 'ready' : 'loading'); - console.log('useClerkQueryClient, queryStatus', queryStatus); - useEffect(() => { - // @ts-expect-error - queryClientStatus is not typed - clerk.on('queryClientStatus', setQueryStatus); - return () => { - // @ts-expect-error - queryClientStatus is not typed - clerk.off('queryClientStatus', setQueryStatus); - }; - }, [clerk]); - - const queryClient = useMemo(() => { - // @ts-expect-error - __internal_queryClient is not typed - console.log('useClerkQueryClient, clerk.__internal_queryClient', clerk.__internal_queryClient); - // @ts-expect-error - __internal_queryClient is not typed - return clerk.__internal_queryClient; - // @ts-expect-error - __internal_queryClient is not typed - }, [queryStatus, clerk.status, clerk.__internal_queryClient]); - - const debouncedQueryStatus = useDebounce( - '__tag' in queryClient && queryClient.__tag === 'clerk-rq-client' ? 'ready' : queryStatus, - 5_000, - ); - console.log('useClerkQueryClient, debouncedQueryStatus', debouncedQueryStatus); - - return [queryClient.client, debouncedQueryStatus]; -}; - -/** - * + * 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`. */ export function useSubscription(params?: UseSubscriptionParams): SubscriptionResult { useAssertWrappedByClerkProvider(hookName); @@ -92,7 +36,7 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes ? environment?.commerceSettings.billing.organization.enabled : environment?.commerceSettings.billing.user.enabled; - const [queryClient, queryStatus] = useClerkQueryClient(); + const queryClient = useClerkQueryClient(); const queryKey = useMemo(() => { return [ @@ -104,21 +48,15 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes ]; }, [user?.id, isOrganization, organization?.id]); - console.log('enabled', Boolean(user?.id && billingEnabled) && clerk.status === 'ready' && queryStatus === 'ready'); - - const query = useQuery( - { - queryKey, - queryFn: ({ queryKey }) => { - const obj = queryKey[1] as { args: { orgId?: string } }; - console.log('queryFn, obj', obj); - return clerk.billing.getSubscription(obj.args); - }, - staleTime: 1_000 * 60, - enabled: Boolean(user?.id && billingEnabled) && clerk.status === 'ready' && queryStatus === 'ready', + const query = useClerkQuery({ + queryKey, + queryFn: ({ queryKey }) => { + const obj = queryKey[1] as { args: { orgId?: string } }; + return clerk.billing.getSubscription(obj.args); }, - queryClient, - ); + staleTime: 1_000 * 60, + enabled: Boolean(user?.id && billingEnabled), + }); const revalidate = useCallback(() => queryClient.invalidateQueries({ queryKey }), [queryClient, queryKey]); diff --git a/packages/shared/src/react/hooks/useSubscription.swr.tsx b/packages/shared/src/react/hooks/useSubscription.swr.tsx index 2261e5cba2c..1a30bb8c67b 100644 --- a/packages/shared/src/react/hooks/useSubscription.swr.tsx +++ b/packages/shared/src/react/hooks/useSubscription.swr.tsx @@ -15,10 +15,11 @@ const hookName = 'useSubscription'; /** * @internal + * This is the existing implementation of useSubscription using SWR. + * It is kept here for backwards compatibility until our next major version. */ export function useSubscription(params?: UseSubscriptionParams): SubscriptionResult { useAssertWrappedByClerkProvider(hookName); - console.log('useSubscription SWR'); const clerk = useClerkInstanceContext(); const user = useUserContext(); diff --git a/packages/shared/src/react/providers/DataClientProvider.rq.tsx b/packages/shared/src/react/providers/DataClientProvider.rq.tsx deleted file mode 100644 index 54d61125af6..00000000000 --- a/packages/shared/src/react/providers/DataClientProvider.rq.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import React, { type PropsWithChildren } from 'react'; - -const queryClient = new QueryClient(); - -/** - * - */ -export function DataClientProvider({ children }: PropsWithChildren<{}>) { - return {children}; -} diff --git a/packages/shared/src/react/providers/DataClientProvider.swr.tsx b/packages/shared/src/react/providers/DataClientProvider.swr.tsx deleted file mode 100644 index 2f781d61907..00000000000 --- a/packages/shared/src/react/providers/DataClientProvider.swr.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React, { type PropsWithChildren } from 'react'; -import { SWRConfig } from 'swr'; - -/** - * - */ -export function DataClientProvider({ children }: PropsWithChildren<{}>) { - return {children}; -} diff --git a/packages/shared/src/react/providers/DataClientProvider.tsx b/packages/shared/src/react/providers/DataClientProvider.tsx deleted file mode 100644 index c12465f1a6b..00000000000 --- a/packages/shared/src/react/providers/DataClientProvider.tsx +++ /dev/null @@ -1 +0,0 @@ -export { DataClientProvider } from 'virtual:data-hooks/DataClientProvider'; diff --git a/packages/shared/src/react/providers/SWRConfigCompat.rq.tsx b/packages/shared/src/react/providers/SWRConfigCompat.rq.tsx new file mode 100644 index 00000000000..06f71e0fec7 --- /dev/null +++ b/packages/shared/src/react/providers/SWRConfigCompat.rq.tsx @@ -0,0 +1,8 @@ +import type { PropsWithChildren } from 'react'; + +/** + * @internal + */ +export function SWRConfigCompat({ children }: PropsWithChildren) { + return <>{children}; +} diff --git a/packages/shared/src/react/providers/SWRConfigCompat.swr.tsx b/packages/shared/src/react/providers/SWRConfigCompat.swr.tsx new file mode 100644 index 00000000000..555d744474b --- /dev/null +++ b/packages/shared/src/react/providers/SWRConfigCompat.swr.tsx @@ -0,0 +1,9 @@ +import React, { type PropsWithChildren } from 'react'; +import { SWRConfig } from 'swr'; + +/** + * @internal + */ +export function SWRConfigCompat({ swrConfig, children }: PropsWithChildren<{ swrConfig?: any }>) { + return {children}; +} diff --git a/packages/shared/src/react/providers/SWRConfigCompat.tsx b/packages/shared/src/react/providers/SWRConfigCompat.tsx new file mode 100644 index 00000000000..0286d80613d --- /dev/null +++ b/packages/shared/src/react/providers/SWRConfigCompat.tsx @@ -0,0 +1 @@ +export { SWRConfigCompat } from 'virtual:data-hooks/SWRConfigCompat'; diff --git a/packages/shared/src/types/virtual-data-hooks.d.ts b/packages/shared/src/types/virtual-data-hooks.d.ts index 4e27d9f888c..0f3065af451 100644 --- a/packages/shared/src/types/virtual-data-hooks.d.ts +++ b/packages/shared/src/types/virtual-data-hooks.d.ts @@ -1,6 +1,6 @@ declare module 'virtual:data-hooks/*' { // Generic export signatures to satisfy type resolution for virtual modules - export const DataClientProvider: any; + export const SWRConfigCompat: any; export const useSubscription: any; const mod: any; export default mod; diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index 91567832fc9..461a9ae89de 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -15,7 +15,8 @@ "lib": ["ES2022", "DOM", "WebWorker"], "allowJs": true, "paths": { - "virtual:data-hooks/*": ["./src/react/hooks/useSubscription.swr.tsx"] + "virtual:data-hooks/useSubscription": ["./src/react/hooks/useSubscription.swr.tsx"], + "virtual:data-hooks/SWRConfigCompat": ["./src/react/providers/SWRConfigCompat.swr.tsx"] } }, "exclude": ["node_modules"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2dd4f21f87c..2d3e62ae51d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -472,7 +472,7 @@ importers: version: 0.5.17 '@tanstack/query-core': specifier: ^5.87.4 - version: 5.90.2 + version: 5.87.4 '@zxcvbn-ts/core': specifier: 3.0.4 version: 3.0.4 @@ -925,9 +925,6 @@ importers: '@clerk/types': specifier: workspace:^ version: link:../types - '@tanstack/react-query': - specifier: ^5.87.4 - version: 5.90.2(react@18.3.1) dequal: specifier: 2.0.3 version: 2.0.3 @@ -956,6 +953,9 @@ importers: '@stripe/stripe-js': specifier: 5.6.0 version: 5.6.0 + '@tanstack/query-core': + specifier: 5.87.4 + version: 5.87.4 '@types/glob-to-regexp': specifier: 0.4.4 version: 0.4.4 @@ -2766,7 +2766,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {'0': node >=0.10.0} + engines: {node: '>=0.10.0'} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} @@ -4889,13 +4889,8 @@ packages: resolution: {integrity: sha512-cs1WKawpXIe+vSTeiZUuSBy8JFjEuDgdMKZFRLKwQysKo8y2q6Q1HvS74Yw+m5IhOW1nTZooa6rlgdfXcgFAaw==} engines: {node: '>=12'} - '@tanstack/query-core@5.90.2': - resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} - - '@tanstack/react-query@5.90.2': - resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} - peerDependencies: - react: ^18 || ^19 + '@tanstack/query-core@5.87.4': + resolution: {integrity: sha512-uNsg6zMxraEPDVO2Bn+F3/ctHi+Zsk+MMpcN8h6P7ozqD088F6mFY5TfGM7zuyIrL7HKpDyu6QHfLWiDxh3cuw==} '@tanstack/react-router@1.131.49': resolution: {integrity: sha512-WHgWJ053W8VU8lUYh8abSHVPeQdpaCpfaUAbV+3uYXbip2G+qlmI/Gsbh/BBV3bYtIi6l3t5dqx3ffCXNTzB5Q==} @@ -19663,12 +19658,7 @@ snapshots: '@tanstack/history@1.131.2': {} - '@tanstack/query-core@5.90.2': {} - - '@tanstack/react-query@5.90.2(react@18.3.1)': - dependencies: - '@tanstack/query-core': 5.90.2 - react: 18.3.1 + '@tanstack/query-core@5.87.4': {} '@tanstack/react-router@1.131.49(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: From 827f5368a150f0dd1779c9236a0c5e67f8999148 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 3 Oct 2025 21:24:49 +0300 Subject: [PATCH 6/8] cleanup --- .../clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx | 2 -- packages/react/src/contexts/ClerkContextProvider.tsx | 1 - packages/react/src/isomorphicClerk.ts | 8 +------- packages/shared/global.d.ts | 8 -------- .../shared/src/react/providers/SWRConfigCompat.rq.tsx | 1 + 5 files changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx b/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx index 81abd3021de..b4389dc9363 100644 --- a/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx +++ b/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx @@ -52,8 +52,6 @@ export function CoreClerkContextWrapper(props: CoreClerkContextWrapperProps): JS diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index 41abf3e4eb6..0e63be1e544 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -96,7 +96,6 @@ export function ClerkContextProvider(props: ClerkContextProvider) { diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 32ff8cc5529..1b2c6fb62a6 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -146,7 +146,6 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { private premountApiKeysNodes = new Map(); private premountOAuthConsentNodes = new Map(); private premountTaskChooseOrganizationNodes = new Map(); - private prefetchQueryClientStatus = false; // A separate Map of `addListener` method calls to handle multiple listeners. private premountAddListenerCalls = new Map< @@ -576,7 +575,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { // @ts-expect-error - queryClientStatus is not typed this.#eventBus.internal.retrieveListeners('queryClientStatus')?.forEach(listener => { - // Since clerkjs exists it will call `this.clerkjs.on('status', listener)` + // Since clerkjs exists it will call `this.clerkjs.on('queryClientStatus', listener)` // @ts-expect-error - queryClientStatus is not typed this.on('queryClientStatus', listener, { notify: true }); }); @@ -625,11 +624,6 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { clerkjs.openWaitlist(this.preOpenWaitlist); } - // if (this.prefetchQueryClientStatus) { - // // @ts-expect-error - queryClientStatus is not typed - // this.clerkjs.getInternalQueryClient?.(); - // } - this.premountSignInNodes.forEach((props, node) => { clerkjs.mountSignIn(node, props); }); diff --git a/packages/shared/global.d.ts b/packages/shared/global.d.ts index 22af55556c5..5776b61ae17 100644 --- a/packages/shared/global.d.ts +++ b/packages/shared/global.d.ts @@ -10,11 +10,3 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv; } - -declare module 'virtual:data-hooks/*' { - // Generic export signatures to satisfy type resolution for virtual modules - export const DataClientProvider: any; - export const useSubscription: any; - const mod: any; - export default mod; -} diff --git a/packages/shared/src/react/providers/SWRConfigCompat.rq.tsx b/packages/shared/src/react/providers/SWRConfigCompat.rq.tsx index 06f71e0fec7..a21ddc663ed 100644 --- a/packages/shared/src/react/providers/SWRConfigCompat.rq.tsx +++ b/packages/shared/src/react/providers/SWRConfigCompat.rq.tsx @@ -1,4 +1,5 @@ import type { PropsWithChildren } from 'react'; +import React from 'react'; /** * @internal From 96b68d277684baae027297b7fefc21731087264b Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 3 Oct 2025 21:25:30 +0300 Subject: [PATCH 7/8] bundlewatch and changeset --- .changeset/tricky-badgers-post.md | 7 +++++++ packages/clerk-js/bundlewatch.config.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/tricky-badgers-post.md diff --git a/.changeset/tricky-badgers-post.md b/.changeset/tricky-badgers-post.md new file mode 100644 index 00000000000..883a5ff001c --- /dev/null +++ b/.changeset/tricky-badgers-post.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +'@clerk/clerk-react': patch +--- + +wip diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 362c61fd573..c7d381954da 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,6 +1,6 @@ { "files": [ - { "path": "./dist/clerk.js", "maxSize": "821KB" }, + { "path": "./dist/clerk.js", "maxSize": "823KB" }, { "path": "./dist/clerk.browser.js", "maxSize": "81KB" }, { "path": "./dist/clerk.legacy.browser.js", "maxSize": "123KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "63KB" }, From c07fe17e2db7ff6d97d0302ff11d6b157e22147e Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 3 Oct 2025 21:35:20 +0300 Subject: [PATCH 8/8] fix lint --- packages/shared/src/react/clerk-rq/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shared/src/react/clerk-rq/types.ts b/packages/shared/src/react/clerk-rq/types.ts index 2b524ab6fb0..6f5bcbfbc8d 100644 --- a/packages/shared/src/react/clerk-rq/types.ts +++ b/packages/shared/src/react/clerk-rq/types.ts @@ -24,6 +24,7 @@ export interface UseBaseQueryOptions< } export type AnyUseQueryOptions = UseQueryOptions; +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface UseQueryOptions< TQueryFnData = unknown, TError = DefaultError,