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/__tests__/index.native.test.ts b/packages/clerk-js/src/__tests__/index.native.test.ts new file mode 100644 index 00000000000..b8b87a6a52c --- /dev/null +++ b/packages/clerk-js/src/__tests__/index.native.test.ts @@ -0,0 +1,53 @@ +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); + }); + + 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 new file mode 100644 index 00000000000..9921093e283 --- /dev/null +++ b/packages/clerk-js/src/index.native.ts @@ -0,0 +1,27 @@ +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, + }); +} + +// Re-export everything from the base entry point to avoid duplicating exports. +export * from './index'; 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