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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/tricky-badgers-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': patch
'@clerk/shared': patch
'@clerk/clerk-react': patch
---

wip
2 changes: 1 addition & 1 deletion packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -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" },
Expand Down
1 change: 1 addition & 0 deletions packages/clerk-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions packages/clerk-js/rspack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
24 changes: 24 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import type {
WaitlistResource,
Web3Provider,
} from '@clerk/types';
import type { QueryClient } from '@tanstack/query-core';

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

Expand Down Expand Up @@ -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'];
Expand All @@ -240,6 +242,28 @@ export class Clerk implements ClerkInterface {
#touchThrottledUntil = 0;
#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();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are options we might want to consider here, like retry logic. Do we want to always retry all errors, or ignore retrying 4xx kind of errors for example? How many retries should we do by default? Is the default retry delay good? Do we want to match these closely to what we have in SWR today, or change them as part of this?

To be clear, the default RQ ones does the job so I don't think we need to, especially at this early stage, just wanted to mention it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want revalidation on focus and the network status things to work, you will also need to call queryClient.mount(). This is normally done in QueryClientProvider but since we are skipping that here we'd need to do it manually if we need it.

This is safe but a noop for native environments.

// @ts-expect-error - queryClientStatus is not typed
this.#publicEventBus.emit('queryClientStatus', 'ready');
});
}

return this.#queryClient
? {
__tag: 'clerk-rq-client', // make this a symbol
client: this.#queryClient,
}
: undefined;
}

public __internal_getCachedResources:
| (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>)
| undefined;
Expand Down
3 changes: 3 additions & 0 deletions packages/clerk-js/src/core/query-core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { QueryClient } from '@tanstack/query-core';

export { QueryClient };
5 changes: 4 additions & 1 deletion packages/react/src/contexts/ClerkContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ export function ClerkContextProvider(props: ClerkContextProvider) {
<IsomorphicClerkContext.Provider value={clerkCtx}>
<ClientContext.Provider value={clientCtx}>
<SessionContext.Provider value={sessionCtx}>
<OrganizationProvider {...organizationCtx.value}>
<OrganizationProvider
// key={clerkStatus + queryStatus}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: ✂️

{...organizationCtx.value}
>
<AuthContext.Provider value={authCtx}>
<UserContext.Provider value={userCtx}>
<CheckoutProvider
Expand Down
13 changes: 13 additions & 0 deletions packages/react/src/isomorphicClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
private premountApiKeysNodes = new Map<HTMLDivElement, APIKeysProps | undefined>();
private premountOAuthConsentNodes = new Map<HTMLDivElement, __internal_OAuthConsentProps | undefined>();
private premountTaskChooseOrganizationNodes = new Map<HTMLDivElement, TaskChooseOrganizationProps | undefined>();

// A separate Map of `addListener` method calls to handle multiple listeners.
private premountAddListenerCalls = new Map<
ListenerCallback,
Expand Down Expand Up @@ -283,6 +284,11 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
return this.clerkjs?.isStandardBrowser || this.options.standardBrowser || false;
}

get __internal_queryClient() {
// @ts-expect-error - __internal_queryClient is not typed
return this.clerkjs?.__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
Expand Down Expand Up @@ -567,6 +573,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('queryClientStatus', listener)`
// @ts-expect-error - queryClientStatus is not typed
this.on('queryClientStatus', listener, { notify: true });
});
Comment on lines +576 to +581
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Ensure queryClientStatus event is properly typed in a follow-up.

The queryClientStatus event propagation uses @ts-expect-error suppressions, indicating the event is not yet part of the type system. While this mirrors the pattern used for status event listeners (lines 571-574), the event name and callback signature should be properly typed to maintain type safety.

Verify that:

  1. The queryClientStatus event is added to the Clerk event type definitions
  2. The listener callback signature is properly typed
  3. A follow-up task exists to remove these @ts-expect-error suppressions

You can run this script to check if the event is documented elsewhere:


🏁 Script executed:

#!/bin/bash
# Search for queryClientStatus event definitions and usages
rg -nC3 "queryClientStatus" --type=ts -g '!*.test.ts'

Length of output: 148


🏁 Script executed:

#!/bin/bash
rg -nC3 "queryClientStatus"

Length of output: 2581


Define and type the queryClientStatus event

Add queryClientStatus to Clerk’s event type definitions and remove the @ts-expect-error suppressions in:

  • packages/clerk-js/src/core/clerk.ts (emit)
  • packages/react/src/isomorphicClerk.ts (propagation)
  • packages/shared/src/react/clerk-rq/use-clerk-query-client.ts (hook)


if (this.preopenSignIn !== null) {
clerkjs.openSignIn(this.preopenSignIn);
}
Expand Down
1 change: 1 addition & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
"devDependencies": {
"@stripe/react-stripe-js": "3.1.1",
"@stripe/stripe-js": "5.6.0",
"@tanstack/query-core": "5.87.4",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Version mismatch with clerk-js package.

@tanstack/query-core is added here as exact version 5.87.4 (devDependency), but packages/clerk-js/package.json line 74 declares it as ^5.87.4 (runtime dependency). The caret range in clerk-js allows minor/patch drift, which can cause inconsistencies if shared's dev tooling expects the exact version. For workspace consistency, align both to the same specifier—either exact 5.87.4 or workspace:^.

Apply this diff to use the workspace protocol for consistency:

-    "@tanstack/query-core": "5.87.4",
+    "@tanstack/query-core": "workspace:^",

Alternatively, ensure both packages use the same exact version or caret range.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@tanstack/query-core": "5.87.4",
"@tanstack/query-core": "workspace:^",
🤖 Prompt for AI Agents
In packages/shared/package.json around line 158, the devDependency for
@tanstack/query-core is pinned to an exact version ("5.87.4") but
packages/clerk-js/package.json declares it as a caret range (^5.87.4), causing a
version specifier mismatch; update packages/shared/package.json to use the
workspace protocol (e.g., "workspace:^5.87.4") or change it to the same
specifier used in clerk-js (e.g., "^5.87.4") so both packages use an identical
dependency specifier to ensure workspace consistency.

"@types/glob-to-regexp": "0.4.4",
"@types/js-cookie": "3.0.6",
"cross-fetch": "^4.1.0",
Expand Down
80 changes: 80 additions & 0 deletions packages/shared/src/react/clerk-rq/queryOptions.ts
Original file line number Diff line number Diff line change
@@ -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<TQueryFnData, TError, TData, TQueryKey> & {
initialData?: undefined | InitialDataFunction<NonUndefinedGuard<TQueryFnData>> | NonUndefinedGuard<TQueryFnData>;
};

export type UnusedSkipTokenOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = OmitKeyof<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'queryFn'> & {
queryFn?: Exclude<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>['queryFn'], SkipToken | undefined>;
};

export type DefinedInitialDataOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'queryFn'> & {
initialData: NonUndefinedGuard<TQueryFnData> | (() => NonUndefinedGuard<TQueryFnData>);
queryFn?: QueryFunction<TQueryFnData, TQueryKey>;
};

export function queryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
): DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> & {
queryKey: DataTag<TQueryKey, TQueryFnData, TError>;
};

export function queryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: UnusedSkipTokenOptions<TQueryFnData, TError, TData, TQueryKey>,
): UnusedSkipTokenOptions<TQueryFnData, TError, TData, TQueryKey> & {
queryKey: DataTag<TQueryKey, TQueryFnData, TError>;
};

export function queryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
): UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> & {
queryKey: DataTag<TQueryKey, TQueryFnData, TError>;
};

/**
*
*/
export function queryOptions(options: unknown) {
return options;
}
54 changes: 54 additions & 0 deletions packages/shared/src/react/clerk-rq/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type {
DefaultError,
DefinedQueryObserverResult,
InfiniteQueryObserverOptions,
OmitKeyof,
QueryKey,
QueryObserverOptions,
QueryObserverResult,
} from '@tanstack/query-core';

export type AnyUseBaseQueryOptions = UseBaseQueryOptions<any, any, any, any, any>;
export interface UseBaseQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> {
/**
* Set this to `false` to unsubscribe this observer from updates to the query cache.
* Defaults to `true`.
*/
subscribed?: boolean;
}

export type AnyUseQueryOptions = UseQueryOptions<any, any, any, any>;
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface UseQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends OmitKeyof<UseBaseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>, 'suspense'> {}

export type AnyUseInfiniteQueryOptions = UseInfiniteQueryOptions<any, any, any, any, any>;
export interface UseInfiniteQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> extends OmitKeyof<InfiniteQueryObserverOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>, 'suspense'> {
/**
* Set this to `false` to unsubscribe this observer from updates to the query cache.
* Defaults to `true`.
*/
subscribed?: boolean;
}

export type UseBaseQueryResult<TData = unknown, TError = DefaultError> = QueryObserverResult<TData, TError>;

export type UseQueryResult<TData = unknown, TError = DefaultError> = UseBaseQueryResult<TData, TError>;

export type DefinedUseQueryResult<TData = unknown, TError = DefaultError> = DefinedQueryObserverResult<TData, TError>;
82 changes: 82 additions & 0 deletions packages/shared/src/react/clerk-rq/use-clerk-query-client.ts
Original file line number Diff line number Diff line change
@@ -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<typeof callableTarget> = {
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 };
Loading
Loading