Skip to content

Commit f5c27e3

Browse files
committed
feat(shared): useSubscription variant with React Query
1 parent 82901ad commit f5c27e3

22 files changed

+382
-229
lines changed

packages/clerk-js/rspack.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ const common = ({ mode, variant, disableRHC = false }) => {
114114
chunks: 'all',
115115
enforce: true,
116116
},
117+
queryCoreVendor: {
118+
test: /[\\/]node_modules[\\/](@tanstack\/query-core)[\\/]/,
119+
name: 'query-core-vendors',
120+
chunks: 'all',
121+
enforce: true,
122+
},
117123
/**
118124
* Sign up is shared between the SignUp component and the SignIn component.
119125
*/

packages/clerk-js/src/core/clerk.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ import type {
9595
WaitlistResource,
9696
Web3Provider,
9797
} from '@clerk/types';
98-
import { QueryClient } from '@tanstack/query-core';
98+
import type { QueryClient } from '@tanstack/query-core';
9999

100100
import { debugLogger, initDebugLogger } from '@/utils/debug';
101101

@@ -223,7 +223,7 @@ export class Clerk implements ClerkInterface {
223223
// converted to protected environment to support `updateEnvironment` type assertion
224224
protected environment?: EnvironmentResource | null;
225225

226-
#queryClient: QueryClient | undefined = new QueryClient();
226+
#queryClient: QueryClient | undefined;
227227
#publishableKey = '';
228228
#domain: DomainOrProxyUrl['domain'];
229229
#proxyUrl: DomainOrProxyUrl['proxyUrl'];
@@ -243,6 +243,19 @@ export class Clerk implements ClerkInterface {
243243
#publicEventBus = createClerkEventBus();
244244

245245
get __internal_queryClient(): { __tag: 'clerk-rq-client'; client: QueryClient } | undefined {
246+
if (!this.#queryClient) {
247+
void import('./query-core')
248+
.then(module => module.QueryClient)
249+
.then(QueryClient => {
250+
if (this.#queryClient) {
251+
return;
252+
}
253+
this.#queryClient = new QueryClient();
254+
// @ts-expect-error - queryClientStatus is not typed
255+
this.#publicEventBus.emit('queryClientStatus', 'ready');
256+
});
257+
}
258+
246259
return this.#queryClient
247260
? {
248261
__tag: 'clerk-rq-client', // make this a symbol
@@ -251,16 +264,6 @@ export class Clerk implements ClerkInterface {
251264
: undefined;
252265
}
253266

254-
public async getInternalQueryClient(): Promise<QueryClient> {
255-
// const QueryClient = await import('./query-core').then(module => module.QueryClient);
256-
if (!this.#queryClient) {
257-
// this.#queryClient = new QueryClient();
258-
// @ts-expect-error - queryClientStatus is not typed
259-
this.#publicEventBus.emit('queryClientStatus', 'ready');
260-
}
261-
return this.#queryClient;
262-
}
263-
264267
public __internal_getCachedResources:
265268
| (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>)
266269
| undefined;

packages/react/src/contexts/ClerkContextProvider.tsx

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,26 +88,6 @@ export function ClerkContextProvider(props: ClerkContextProvider) {
8888
return { value };
8989
}, [orgId, organization]);
9090

91-
// const [queryStatus, setQueryStatus] = React.useState('loading');
92-
93-
// React.useEffect(() => {
94-
// // @ts-expect-error - queryClientStatus is not typed
95-
// clerk.on('queryClientStatus', (e)=>{
96-
// console.log('on queryClientStatus', e);
97-
// setQueryStatus(e);
98-
// });
99-
// return () => {
100-
// // @ts-expect-error - queryClientStatus is not typed
101-
// clerk.off('queryClientStatus', setQueryStatus);
102-
// };
103-
// }, [clerk]);
104-
105-
// const queryClient = React.useMemo(() => {
106-
// return clerk.__internal_queryClient;
107-
// }, [queryStatus, clerkStatus]);
108-
109-
// console.log('queryStatus', queryStatus, queryClient);
110-
11191
return (
11292
// @ts-expect-error value passed is of type IsomorphicClerk where the context expects LoadedClerk
11393
<IsomorphicClerkContext.Provider value={clerkCtx}>

packages/react/src/isomorphicClerk.ts

Lines changed: 5 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -115,66 +115,6 @@ type IsomorphicLoadedClerk = Without<
115115
apiKeys: APIKeysNamespace | undefined;
116116
};
117117

118-
export type RecursiveMock = {
119-
(...args: unknown[]): RecursiveMock;
120-
} & {
121-
readonly [key in string | symbol]: RecursiveMock;
122-
};
123-
124-
/**
125-
* Creates a recursively self-referential Proxy that safely handles:
126-
* - Arbitrary property access (e.g., obj.any.prop.path)
127-
* - Function calls at any level (e.g., obj.a().b.c())
128-
* - Construction (e.g., new obj.a.b())
129-
*
130-
* Always returns itself to allow infinite chaining without throwing.
131-
*/
132-
function createRecursiveProxy(label: string = 'Mock'): RecursiveMock {
133-
// The callable target for the proxy so that `apply` works
134-
const callableTarget = function noop(): void {};
135-
136-
// eslint-disable-next-line prefer-const
137-
let self: RecursiveMock;
138-
const handler: ProxyHandler<typeof callableTarget> = {
139-
get(_target, prop) {
140-
// Avoid being treated as a Promise/thenable by test runners or frameworks
141-
if (prop === 'then') {
142-
return undefined;
143-
}
144-
if (prop === 'toString') {
145-
return () => `[${label}]`;
146-
}
147-
if (prop === Symbol.toPrimitive) {
148-
return () => 0;
149-
}
150-
return self;
151-
},
152-
apply() {
153-
return self;
154-
},
155-
construct() {
156-
return self as unknown as object;
157-
},
158-
has() {
159-
return true;
160-
},
161-
set() {
162-
return true;
163-
},
164-
};
165-
166-
self = new Proxy(callableTarget, handler) as unknown as RecursiveMock;
167-
return self;
168-
}
169-
170-
/**
171-
* Returns a permissive mock compatible with `QueryClient` usage in tests.
172-
* It accepts any chain of property accesses and calls without throwing.
173-
*/
174-
export function createMockQueryClient(): RecursiveMock {
175-
return createRecursiveProxy('MockQueryClient') as unknown as RecursiveMock;
176-
}
177-
178118
export class IsomorphicClerk implements IsomorphicLoadedClerk {
179119
private readonly mode: 'browser' | 'server';
180120
private readonly options: IsomorphicClerkOptions;
@@ -224,7 +164,6 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
224164
#publishableKey: string;
225165
#eventBus = createClerkEventBus();
226166
#stateProxy: StateProxy;
227-
#__internal_queryClient = createMockQueryClient();
228167

229168
get publishableKey(): string {
230169
return this.#publishableKey;
@@ -348,14 +287,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
348287

349288
get __internal_queryClient() {
350289
// @ts-expect-error - __internal_queryClient is not typed
351-
if (!this.clerkjs?.__internal_queryClient) {
352-
// @ts-expect-error - __internal_queryClient is not typed
353-
void this.clerkjs?.getInternalQueryClient?.();
354-
this.prefetchQueryClientStatus = true;
355-
}
356-
357-
// @ts-expect-error - __internal_queryClient is not typed
358-
return this.clerkjs?.__internal_queryClient || this.#__internal_queryClient;
290+
return this.clerkjs?.__internal_queryClient;
359291
}
360292

361293
get isSatellite() {
@@ -693,10 +625,10 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
693625
clerkjs.openWaitlist(this.preOpenWaitlist);
694626
}
695627

696-
if (this.prefetchQueryClientStatus) {
697-
// @ts-expect-error - queryClientStatus is not typed
698-
this.clerkjs.getInternalQueryClient?.();
699-
}
628+
// if (this.prefetchQueryClientStatus) {
629+
// // @ts-expect-error - queryClientStatus is not typed
630+
// this.clerkjs.getInternalQueryClient?.();
631+
// }
700632

701633
this.premountSignInNodes.forEach((props, node) => {
702634
clerkjs.mountSignIn(node, props);

packages/shared/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@
146146
},
147147
"dependencies": {
148148
"@clerk/types": "workspace:^",
149-
"@tanstack/react-query": "^5.87.4",
150149
"dequal": "2.0.3",
151150
"glob-to-regexp": "0.4.1",
152151
"js-cookie": "3.0.5",
@@ -156,6 +155,7 @@
156155
"devDependencies": {
157156
"@stripe/react-stripe-js": "3.1.1",
158157
"@stripe/stripe-js": "5.6.0",
158+
"@tanstack/query-core": "5.87.4",
159159
"@types/glob-to-regexp": "0.4.4",
160160
"@types/js-cookie": "3.0.6",
161161
"cross-fetch": "^4.1.0",
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type {
2+
DataTag,
3+
DefaultError,
4+
InitialDataFunction,
5+
NonUndefinedGuard,
6+
OmitKeyof,
7+
QueryFunction,
8+
QueryKey,
9+
SkipToken,
10+
} from '@tanstack/query-core';
11+
12+
import type { UseQueryOptions } from './types';
13+
14+
export type UndefinedInitialDataOptions<
15+
TQueryFnData = unknown,
16+
TError = DefaultError,
17+
TData = TQueryFnData,
18+
TQueryKey extends QueryKey = QueryKey,
19+
> = UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
20+
initialData?: undefined | InitialDataFunction<NonUndefinedGuard<TQueryFnData>> | NonUndefinedGuard<TQueryFnData>;
21+
};
22+
23+
export type UnusedSkipTokenOptions<
24+
TQueryFnData = unknown,
25+
TError = DefaultError,
26+
TData = TQueryFnData,
27+
TQueryKey extends QueryKey = QueryKey,
28+
> = OmitKeyof<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'queryFn'> & {
29+
queryFn?: Exclude<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>['queryFn'], SkipToken | undefined>;
30+
};
31+
32+
export type DefinedInitialDataOptions<
33+
TQueryFnData = unknown,
34+
TError = DefaultError,
35+
TData = TQueryFnData,
36+
TQueryKey extends QueryKey = QueryKey,
37+
> = Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'queryFn'> & {
38+
initialData: NonUndefinedGuard<TQueryFnData> | (() => NonUndefinedGuard<TQueryFnData>);
39+
queryFn?: QueryFunction<TQueryFnData, TQueryKey>;
40+
};
41+
42+
export function queryOptions<
43+
TQueryFnData = unknown,
44+
TError = DefaultError,
45+
TData = TQueryFnData,
46+
TQueryKey extends QueryKey = QueryKey,
47+
>(
48+
options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
49+
): DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> & {
50+
queryKey: DataTag<TQueryKey, TQueryFnData, TError>;
51+
};
52+
53+
export function queryOptions<
54+
TQueryFnData = unknown,
55+
TError = DefaultError,
56+
TData = TQueryFnData,
57+
TQueryKey extends QueryKey = QueryKey,
58+
>(
59+
options: UnusedSkipTokenOptions<TQueryFnData, TError, TData, TQueryKey>,
60+
): UnusedSkipTokenOptions<TQueryFnData, TError, TData, TQueryKey> & {
61+
queryKey: DataTag<TQueryKey, TQueryFnData, TError>;
62+
};
63+
64+
export function queryOptions<
65+
TQueryFnData = unknown,
66+
TError = DefaultError,
67+
TData = TQueryFnData,
68+
TQueryKey extends QueryKey = QueryKey,
69+
>(
70+
options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
71+
): UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> & {
72+
queryKey: DataTag<TQueryKey, TQueryFnData, TError>;
73+
};
74+
75+
/**
76+
*
77+
*/
78+
export function queryOptions(options: unknown) {
79+
return options;
80+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type {
2+
DefaultError,
3+
DefinedQueryObserverResult,
4+
InfiniteQueryObserverOptions,
5+
OmitKeyof,
6+
QueryKey,
7+
QueryObserverOptions,
8+
QueryObserverResult,
9+
} from '@tanstack/query-core';
10+
11+
export type AnyUseBaseQueryOptions = UseBaseQueryOptions<any, any, any, any, any>;
12+
export interface UseBaseQueryOptions<
13+
TQueryFnData = unknown,
14+
TError = DefaultError,
15+
TData = TQueryFnData,
16+
TQueryData = TQueryFnData,
17+
TQueryKey extends QueryKey = QueryKey,
18+
> extends QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> {
19+
/**
20+
* Set this to `false` to unsubscribe this observer from updates to the query cache.
21+
* Defaults to `true`.
22+
*/
23+
subscribed?: boolean;
24+
}
25+
26+
export type AnyUseQueryOptions = UseQueryOptions<any, any, any, any>;
27+
export interface UseQueryOptions<
28+
TQueryFnData = unknown,
29+
TError = DefaultError,
30+
TData = TQueryFnData,
31+
TQueryKey extends QueryKey = QueryKey,
32+
> extends OmitKeyof<UseBaseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>, 'suspense'> {}
33+
34+
export type AnyUseInfiniteQueryOptions = UseInfiniteQueryOptions<any, any, any, any, any>;
35+
export interface UseInfiniteQueryOptions<
36+
TQueryFnData = unknown,
37+
TError = DefaultError,
38+
TData = TQueryFnData,
39+
TQueryKey extends QueryKey = QueryKey,
40+
TPageParam = unknown,
41+
> extends OmitKeyof<InfiniteQueryObserverOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>, 'suspense'> {
42+
/**
43+
* Set this to `false` to unsubscribe this observer from updates to the query cache.
44+
* Defaults to `true`.
45+
*/
46+
subscribed?: boolean;
47+
}
48+
49+
export type UseBaseQueryResult<TData = unknown, TError = DefaultError> = QueryObserverResult<TData, TError>;
50+
51+
export type UseQueryResult<TData = unknown, TError = DefaultError> = UseBaseQueryResult<TData, TError>;
52+
53+
export type DefinedUseQueryResult<TData = unknown, TError = DefaultError> = DefinedQueryObserverResult<TData, TError>;

0 commit comments

Comments
 (0)