Skip to content

Commit 1c27d4d

Browse files
authored
refactor(shared,clerk-js,react): move QueryClient ownership into @clerk/shared (#8434)
1 parent 5b4c574 commit 1c27d4d

17 files changed

Lines changed: 221 additions & 154 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@clerk/shared': patch
3+
'@clerk/clerk-js': patch
4+
'@clerk/react': patch
5+
---
6+
7+
Move ownership of the clerk-rq `QueryClient` from `@clerk/clerk-js` into `@clerk/shared`. The `QueryObserver` (constructed in `@clerk/shared`) and the `Query` objects it observes now always come from a single `@tanstack/query-core` resolution — the cross-bundle API contract that produced #8428 (`Query.isFetched is not a function`) no longer exists.
8+
9+
This removes the undocumented `clerk.__internal_queryClient` getter from both `@clerk/clerk-js` and `@clerk/react`'s `IsomorphicClerk`. The `QueryClient` is owned by an internal singleton in `@clerk/shared`, lazily instantiated on the browser only — server renders return `undefined`, preserving SSR safety and avoiding cross-request cache sharing.
10+
11+
`@tanstack/query-core` is no longer a direct dependency of `@clerk/clerk-js`; it remains a dep of `@clerk/shared` and resolves consumer-side as before.

packages/clerk-js/bundlewatch.config.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
{ "path": "./dist/coinbase*.js", "maxSize": "36KB" },
1010
{ "path": "./dist/base-account-sdk*.js", "maxSize": "203KB" },
1111
{ "path": "./dist/stripe-vendors*.js", "maxSize": "1KB" },
12-
{ "path": "./dist/query-core-vendors*.js", "maxSize": "11KB" },
1312
{ "path": "./dist/zxcvbn-ts-core*.js", "maxSize": "12KB" },
1413
{ "path": "./dist/zxcvbn-common*.js", "maxSize": "226KB" }
1514
]

packages/clerk-js/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@
9191
"@solana/wallet-standard": "catalog:module-manager",
9292
"@stripe/stripe-js": "5.6.0",
9393
"@swc/helpers": "catalog:repo",
94-
"@tanstack/query-core": "catalog:repo",
9594
"@wallet-standard/core": "catalog:module-manager",
9695
"@zxcvbn-ts/core": "catalog:module-manager",
9796
"@zxcvbn-ts/language-common": "catalog:module-manager",

packages/clerk-js/rspack.config.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,6 @@ const common = ({ mode, variant, disableRHC = false }) => {
110110
chunks: 'all',
111111
enforce: true,
112112
},
113-
queryCoreVendor: {
114-
test: /[\\/]node_modules[\\/](@tanstack\/query-core)[\\/]/,
115-
name: 'query-core-vendors',
116-
chunks: 'all',
117-
enforce: true,
118-
},
119113
defaultVendors: {
120114
minChunks: 1,
121115
test: module => {

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

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ import type {
138138
import type { ClerkUI } from '@clerk/shared/ui';
139139
import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url';
140140
import { allSettled, handleValueOrFn, noop } from '@clerk/shared/utils';
141-
import type { QueryClient } from '@tanstack/query-core';
142141

143142
import { debugLogger, initDebugLogger } from '@/utils/debug';
144143
import { ModuleManager } from '@/utils/moduleManager';
@@ -248,7 +247,6 @@ export class Clerk implements ClerkInterface {
248247
// converted to protected environment to support `updateEnvironment` type assertion
249248
protected environment?: EnvironmentResource | null;
250249

251-
#queryClient: QueryClient | undefined;
252250
#publishableKey = '';
253251
#domain: DomainOrProxyUrl['domain'];
254252
#proxyUrl: DomainOrProxyUrl['proxyUrl'];
@@ -268,28 +266,6 @@ export class Clerk implements ClerkInterface {
268266
#touchThrottledUntil = 0;
269267
#publicEventBus = createClerkEventBus();
270268

271-
get __internal_queryClient(): { __tag: 'clerk-rq-client'; client: QueryClient } | undefined {
272-
if (!this.#queryClient) {
273-
void import('./query-core')
274-
.then(module => module.QueryClient)
275-
.then(QueryClient => {
276-
if (this.#queryClient) {
277-
return;
278-
}
279-
this.#queryClient = new QueryClient();
280-
// @ts-expect-error - queryClientStatus is not typed
281-
this.#publicEventBus.emit('queryClientStatus', 'ready');
282-
});
283-
}
284-
285-
return this.#queryClient
286-
? {
287-
__tag: 'clerk-rq-client',
288-
client: this.#queryClient,
289-
}
290-
: undefined;
291-
}
292-
293269
public __internal_getCachedResources:
294270
| (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>)
295271
| undefined;

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

Lines changed: 0 additions & 3 deletions
This file was deleted.

packages/clerk-js/src/test/mock-helpers.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { __createClerkTestQueryClient } from '@clerk/shared/react';
12
import type { ActiveSessionResource, LoadedClerk } from '@clerk/shared/types';
23
import { type Mocked, vi } from 'vitest';
34

4-
import { QueryClient } from '../core/query-core';
55
import type { RouteContextValue } from '../ui/router';
66

77
type FunctionLike = (...args: any) => any;
@@ -46,19 +46,7 @@ export const mockClerkMethods = (clerk: LoadedClerk): DeepVitestMocked<LoadedCle
4646
// Cast clerk to any to allow mocking properties
4747
const clerkAny = clerk as any;
4848

49-
const defaultQueryClient = {
50-
__tag: 'clerk-rq-client' as const,
51-
client: new QueryClient({
52-
defaultOptions: {
53-
queries: {
54-
retry: false,
55-
// Setting staleTime to Infinity will not cause issues between tests as long as each test
56-
// case has its own wrapper that initializes a Clerk instance with a new QueryClient.
57-
staleTime: Infinity,
58-
},
59-
},
60-
}),
61-
};
49+
__createClerkTestQueryClient();
6250

6351
mockMethodsOf(clerkAny);
6452
if (clerkAny.client) {
@@ -92,12 +80,6 @@ export const mockClerkMethods = (clerk: LoadedClerk): DeepVitestMocked<LoadedCle
9280
mockMethodsOf(clerkAny.billing);
9381
}
9482

95-
// Mock the __internal_queryClient getter property
96-
Object.defineProperty(clerkAny, '__internal_queryClient', {
97-
get: vi.fn(() => defaultQueryClient),
98-
configurable: true,
99-
});
100-
10183
mockProp(clerkAny, 'navigate');
10284
mockProp(clerkAny, 'setActive');
10385
mockProp(clerkAny, 'redirectWithAuth');

packages/react/src/isomorphicClerk.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -344,11 +344,6 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
344344
return this.clerkjs?.isStandardBrowser || this.options.standardBrowser || false;
345345
}
346346

347-
get __internal_queryClient() {
348-
// @ts-expect-error - __internal_queryClient is not typed
349-
return this.clerkjs?.__internal_queryClient;
350-
}
351-
352347
get isSatellite() {
353348
// This getter can run in environments where window is not available.
354349
// In those cases we should expect and use domain as a string
@@ -656,13 +651,6 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
656651
this.on('status', listener, { notify: true });
657652
});
658653

659-
// @ts-expect-error - queryClientStatus is not typed
660-
this.#eventBus.internal.retrieveListeners('queryClientStatus')?.forEach(listener => {
661-
// Since clerkjs exists it will call `this.clerkjs.on('queryClientStatus', listener)`
662-
// @ts-expect-error - queryClientStatus is not typed
663-
this.on('queryClientStatus', listener, { notify: true });
664-
});
665-
666654
if (this.preopenSignIn !== null) {
667655
clerkjs.openSignIn(this.preopenSignIn);
668656
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { QueryClient } from '@tanstack/query-core';
2+
import { afterEach, describe, expect, it, vi } from 'vitest';
3+
4+
import {
5+
__createClerkTestQueryClient,
6+
__resetClerkQueryClientForTest,
7+
__setClerkQueryClientForTest,
8+
getClerkQueryClient,
9+
} from '../clerk-query-client';
10+
11+
afterEach(() => {
12+
vi.unstubAllGlobals();
13+
__resetClerkQueryClientForTest();
14+
});
15+
16+
describe('getClerkQueryClient', () => {
17+
it('returns undefined when window is not defined (SSR)', () => {
18+
vi.stubGlobal('window', undefined);
19+
20+
expect(getClerkQueryClient()).toBeUndefined();
21+
});
22+
23+
it('does not cache the SSR undefined — a later browser call still creates a client', () => {
24+
vi.stubGlobal('window', undefined);
25+
expect(getClerkQueryClient()).toBeUndefined();
26+
27+
vi.unstubAllGlobals();
28+
const client = getClerkQueryClient();
29+
expect(client).toBeInstanceOf(QueryClient);
30+
});
31+
32+
it('lazy-creates a singleton on the browser and returns the same instance on repeated calls', () => {
33+
const first = getClerkQueryClient();
34+
const second = getClerkQueryClient();
35+
36+
expect(first).toBeInstanceOf(QueryClient);
37+
expect(second).toBe(first);
38+
});
39+
});
40+
41+
describe('__resetClerkQueryClientForTest', () => {
42+
it('clears the singleton so the next read lazy-creates a fresh client', () => {
43+
const original = getClerkQueryClient();
44+
expect(original).toBeInstanceOf(QueryClient);
45+
46+
__resetClerkQueryClientForTest();
47+
48+
const next = getClerkQueryClient();
49+
expect(next).toBeInstanceOf(QueryClient);
50+
expect(next).not.toBe(original);
51+
});
52+
});
53+
54+
describe('__setClerkQueryClientForTest', () => {
55+
it('installs a caller-supplied client and returns it from getClerkQueryClient', () => {
56+
const custom = new QueryClient();
57+
__setClerkQueryClientForTest(custom);
58+
59+
expect(getClerkQueryClient()).toBe(custom);
60+
});
61+
62+
it('installs the "no client" state without triggering lazy creation on subsequent reads', () => {
63+
__setClerkQueryClientForTest(undefined);
64+
65+
expect(getClerkQueryClient()).toBeUndefined();
66+
expect(getClerkQueryClient()).toBeUndefined();
67+
});
68+
});
69+
70+
describe('__createClerkTestQueryClient', () => {
71+
it('returns a QueryClient with deterministic defaults and installs it as the singleton', () => {
72+
const client = __createClerkTestQueryClient();
73+
74+
expect(client).toBeInstanceOf(QueryClient);
75+
expect(getClerkQueryClient()).toBe(client);
76+
77+
const defaults = client.getDefaultOptions().queries;
78+
expect(defaults?.retry).toBe(false);
79+
expect(defaults?.staleTime).toBe(Infinity);
80+
expect(defaults?.refetchOnWindowFocus).toBe(false);
81+
expect(defaults?.refetchOnReconnect).toBe(false);
82+
expect(defaults?.refetchOnMount).toBe(false);
83+
});
84+
});

packages/shared/src/react/clerk-rq/__tests__/useBaseQuery.spec.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from 'react';
33
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
44

55
import { createMockClerk, createMockQueryClient } from '../../hooks/__tests__/mocks/clerk';
6+
import { __resetClerkQueryClientForTest, __setClerkQueryClientForTest } from '../clerk-query-client';
67
import { useClerkInfiniteQuery } from '../useInfiniteQuery';
78
import { useClerkQuery } from '../useQuery';
89

@@ -16,22 +17,15 @@ vi.mock('../../contexts', () => ({
1617

1718
const wrapper = ({ children }: { children: React.ReactNode }) => <>{children}</>;
1819

19-
const makeClerkWithoutQueryClient = () => {
20-
const mockClerk = createMockClerk({ queryClient: null });
21-
Object.defineProperty(mockClerk, '__internal_queryClient', {
22-
get: () => undefined,
23-
configurable: true,
24-
});
25-
return mockClerk;
26-
};
27-
2820
afterEach(() => {
2921
vi.clearAllMocks();
22+
__resetClerkQueryClientForTest();
3023
});
3124

3225
describe('useBaseQuery - dummy result while query client is not attached', () => {
3326
beforeEach(() => {
34-
activeClerk = makeClerkWithoutQueryClient();
27+
activeClerk = createMockClerk({ queryClient: null });
28+
__setClerkQueryClientForTest(undefined);
3529
});
3630

3731
it('reports isLoading: true when the query would be enabled', () => {
@@ -109,8 +103,8 @@ describe('useBaseQuery - dummy result while query client is not attached', () =>
109103

110104
describe('useBaseQuery - normal behavior once query client attaches', () => {
111105
it('delegates to the real observer when the query client is loaded', async () => {
112-
const queryClient = createMockQueryClient();
113-
activeClerk = createMockClerk({ queryClient });
106+
createMockQueryClient();
107+
activeClerk = createMockClerk({ queryClient: undefined });
114108

115109
const queryFn = vi.fn(async () => 'result');
116110
const { result } = renderHook(

0 commit comments

Comments
 (0)