From 5ca75db0d9d71611b53764295dc2d4a50c1aeba7 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 26 Mar 2026 13:23:25 -0500 Subject: [PATCH 1/3] fix(clerk-js): static QueryClient import in native entry point Move the synchronous QueryClient fix from the Expo package into clerk-js itself by creating a dedicated native entry point (index.native.ts) that statically imports QueryClient and overrides the lazy getter on the Clerk prototype. This fixes the root cause at the bundler level without affecting other dynamic imports. --- .changeset/fix-query-client-rn.md | 4 +- packages/clerk-js/rspack.config.js | 2 +- packages/clerk-js/src/index.native.ts | 44 +++++++++++++++++++ packages/expo/package.json | 1 - .../provider/singleton/createClerkInstance.ts | 20 --------- pnpm-lock.yaml | 3 -- 6 files changed, 47 insertions(+), 27 deletions(-) create mode 100644 packages/clerk-js/src/index.native.ts diff --git a/.changeset/fix-query-client-rn.md b/.changeset/fix-query-client-rn.md index 2ac86035d4a..b6e47afa4b3 100644 --- a/.changeset/fix-query-client-rn.md +++ b/.changeset/fix-query-client-rn.md @@ -1,5 +1,5 @@ --- -"@clerk/expo": patch +"@clerk/clerk-js": patch --- -Fix `useOrganizationList` and other query-based hooks returning empty data on React Native by synchronously providing a `QueryClient` instance +Fix `useOrganizationList` and other query-based hooks returning empty data on React Native by statically importing `QueryClient` in the native bundle entry point diff --git a/packages/clerk-js/rspack.config.js b/packages/clerk-js/rspack.config.js index af1e2548990..682b4764e22 100644 --- a/packages/clerk-js/rspack.config.js +++ b/packages/clerk-js/rspack.config.js @@ -22,7 +22,7 @@ const variantToSourceFile = { [variants.clerk]: './src/index.ts', [variants.clerkNoRHC]: './src/index.ts', [variants.clerkBrowser]: './src/index.browser.ts', - [variants.clerkNative]: './src/index.ts', + [variants.clerkNative]: './src/index.native.ts', [variants.clerkLegacyBrowser]: './src/index.legacy.browser.ts', }; diff --git a/packages/clerk-js/src/index.native.ts b/packages/clerk-js/src/index.native.ts new file mode 100644 index 00000000000..b5ca29e02a0 --- /dev/null +++ b/packages/clerk-js/src/index.native.ts @@ -0,0 +1,44 @@ +import { QueryClient } from '@tanstack/query-core'; + +import { Clerk } from './core/clerk'; + +// The default Clerk class lazily loads QueryClient via a dynamic import. +// In React Native, rspack's chunk loading doesn't work (Metro bundles into +// a single file), so the dynamic import never resolves and +// __internal_queryClient stays undefined — breaking hooks that depend on it. +// Override the getter to synchronously create a QueryClient on first access. +const originalDescriptor = Object.getOwnPropertyDescriptor(Clerk.prototype, '__internal_queryClient'); +if (originalDescriptor) { + const queryClientMap = new WeakMap, QueryClient>(); + Object.defineProperty(Clerk.prototype, '__internal_queryClient', { + get(this: InstanceType) { + let client = queryClientMap.get(this); + if (!client) { + client = new QueryClient(); + queryClientMap.set(this, client); + } + return { __tag: 'clerk-rq-client', client }; + }, + configurable: true, + }); +} + +export { + ClerkAPIResponseError, + ClerkRuntimeError, + EmailLinkError, + EmailLinkErrorCode, + EmailLinkErrorCodeStatus, + isClerkAPIResponseError, + isClerkRuntimeError, + isEmailLinkError, + isKnownError, + isMetamaskError, + isUserLockedError, + type MetamaskError, +} from '@clerk/shared/error'; +export { Clerk }; + +if (module.hot) { + module.hot.accept(); +} diff --git a/packages/expo/package.json b/packages/expo/package.json index ca41c50ef8c..294cca6394d 100644 --- a/packages/expo/package.json +++ b/packages/expo/package.json @@ -114,7 +114,6 @@ "@clerk/clerk-js": "workspace:^", "@clerk/react": "workspace:^", "@clerk/shared": "workspace:^", - "@tanstack/query-core": "5.90.16", "base-64": "^1.0.0", "react-native-url-polyfill": "2.0.0", "tslib": "catalog:repo" diff --git a/packages/expo/src/provider/singleton/createClerkInstance.ts b/packages/expo/src/provider/singleton/createClerkInstance.ts index e3ff934b468..2a361bad54a 100644 --- a/packages/expo/src/provider/singleton/createClerkInstance.ts +++ b/packages/expo/src/provider/singleton/createClerkInstance.ts @@ -112,26 +112,6 @@ export function createClerkInstance(ClerkClass: typeof Clerk) { __internal_clerkOptions = { publishableKey, proxyUrl, domain }; __internal_clerk = new ClerkClass(publishableKey, { proxyUrl, domain }) as unknown as BrowserClerk; - // The clerk-js native bundle uses rspack code-splitting for the internal QueryClient. - // On React Native, rspack's chunk loading doesn't work (Metro bundles into a single file), - // so the dynamic import never resolves and __internal_queryClient stays undefined. - // This breaks hooks that depend on the query client (useOrganizationList, etc.). - // Override the getter to synchronously create a QueryClient on first access. - { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { QueryClient } = require('@tanstack/query-core'); - let queryClient: InstanceType | undefined; - Object.defineProperty(__internal_clerk, '__internal_queryClient', { - get() { - if (!queryClient) { - queryClient = new QueryClient(); - } - return { __tag: 'clerk-rq-client', client: queryClient }; - }, - configurable: true, - }); - } - if (Platform.OS === 'ios' || Platform.OS === 'android') { // @ts-expect-error - This is an internal API __internal_clerk.__internal_createPublicCredentials = ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 152dbdd856f..5d62235d895 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -574,9 +574,6 @@ importers: '@clerk/shared': specifier: workspace:^ version: link:../shared - '@tanstack/query-core': - specifier: 5.90.16 - version: 5.90.16 base-64: specifier: ^1.0.0 version: 1.0.0 From e1d72d556bd022bc8a987ef515c6673c4ab9297d Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 26 Mar 2026 13:45:22 -0500 Subject: [PATCH 2/3] test(clerk-js): add unit tests for native entry point QueryClient override --- .../src/__tests__/index.native.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/clerk-js/src/__tests__/index.native.test.ts diff --git a/packages/clerk-js/src/__tests__/index.native.test.ts b/packages/clerk-js/src/__tests__/index.native.test.ts new file mode 100644 index 00000000000..75fc6795261 --- /dev/null +++ b/packages/clerk-js/src/__tests__/index.native.test.ts @@ -0,0 +1,43 @@ +import { QueryClient } from '@tanstack/query-core'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('../core/resources/Client'); +vi.mock('../core/resources/Environment'); + +describe('index.native', () => { + beforeEach(() => { + vi.resetModules(); + }); + + it('overrides __internal_queryClient to return synchronously', async () => { + const { Clerk } = await import('../index.native'); + const clerk = new Clerk('pk_test_Y2xlcmsuZXhhbXBsZS5jb20k'); + + const result = clerk.__internal_queryClient; + + expect(result).toBeDefined(); + expect(result?.__tag).toBe('clerk-rq-client'); + expect(result?.client).toBeInstanceOf(QueryClient); + }); + + it('returns the same QueryClient instance on subsequent accesses', async () => { + const { Clerk } = await import('../index.native'); + const clerk = new Clerk('pk_test_Y2xlcmsuZXhhbXBsZS5jb20k'); + + const first = clerk.__internal_queryClient; + const second = clerk.__internal_queryClient; + + expect(first?.client).toBe(second?.client); + }); + + it('returns different QueryClient instances for different Clerk instances', async () => { + const { Clerk } = await import('../index.native'); + const clerk1 = new Clerk('pk_test_Y2xlcmsuZXhhbXBsZS5jb20k'); + const clerk2 = new Clerk('pk_test_Y2xlcmsuZXhhbXBsZS5jb20k'); + + const client1 = clerk1.__internal_queryClient; + const client2 = clerk2.__internal_queryClient; + + expect(client1?.client).not.toBe(client2?.client); + }); +}); From 0b63c1c14be323190a5eaf51e15449acb45c0129 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 26 Mar 2026 13:47:33 -0500 Subject: [PATCH 3/3] refactor(clerk-js): re-export from base entry point and add export parity test Eliminate duplicated exports in index.native.ts by using 'export * from ./index'. Add a test that verifies the native entry point exports match the base entry point. --- .../src/__tests__/index.native.test.ts | 10 +++++++++ packages/clerk-js/src/index.native.ts | 21 ++----------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/clerk-js/src/__tests__/index.native.test.ts b/packages/clerk-js/src/__tests__/index.native.test.ts index 75fc6795261..b8b87a6a52c 100644 --- a/packages/clerk-js/src/__tests__/index.native.test.ts +++ b/packages/clerk-js/src/__tests__/index.native.test.ts @@ -40,4 +40,14 @@ describe('index.native', () => { expect(client1?.client).not.toBe(client2?.client); }); + + it('exports the same public API as the base entry point', async () => { + const nativeExports = await import('../index.native'); + const baseExports = await import('../index'); + + const nativeKeys = Object.keys(nativeExports).sort(); + const baseKeys = Object.keys(baseExports).sort(); + + expect(nativeKeys).toEqual(baseKeys); + }); }); diff --git a/packages/clerk-js/src/index.native.ts b/packages/clerk-js/src/index.native.ts index b5ca29e02a0..9921093e283 100644 --- a/packages/clerk-js/src/index.native.ts +++ b/packages/clerk-js/src/index.native.ts @@ -23,22 +23,5 @@ if (originalDescriptor) { }); } -export { - ClerkAPIResponseError, - ClerkRuntimeError, - EmailLinkError, - EmailLinkErrorCode, - EmailLinkErrorCodeStatus, - isClerkAPIResponseError, - isClerkRuntimeError, - isEmailLinkError, - isKnownError, - isMetamaskError, - isUserLockedError, - type MetamaskError, -} from '@clerk/shared/error'; -export { Clerk }; - -if (module.hot) { - module.hot.accept(); -} +// Re-export everything from the base entry point to avoid duplicating exports. +export * from './index';