From 85e7571db823a7ca7865054b60b8d78b4f7a3602 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 4 Aug 2026 20:25:08 -0700 Subject: [PATCH 1/3] fix(react): Detect nested ClerkProvider via context instead of a global mount counter --- .changeset/spotty-planes-nest.md | 5 ++ packages/react/src/contexts/ClerkProvider.tsx | 14 +++- .../__tests__/ClerkProvider.nesting.test.tsx | 65 +++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 .changeset/spotty-planes-nest.md create mode 100644 packages/react/src/contexts/__tests__/ClerkProvider.nesting.test.tsx diff --git a/.changeset/spotty-planes-nest.md b/.changeset/spotty-planes-nest.md new file mode 100644 index 00000000000..a9a017413c3 --- /dev/null +++ b/.changeset/spotty-planes-nest.md @@ -0,0 +1,5 @@ +--- +'@clerk/react': patch +--- + +Fix a false-positive "multiple ``" crash in apps that run more than one React root in a single JavaScript runtime, most commonly React Native Android apps during activity recreation. `` now throws this error only when it is genuinely nested inside another ``. diff --git a/packages/react/src/contexts/ClerkProvider.tsx b/packages/react/src/contexts/ClerkProvider.tsx index a2ac25bcd8c..739b7a571cc 100644 --- a/packages/react/src/contexts/ClerkProvider.tsx +++ b/packages/react/src/contexts/ClerkProvider.tsx @@ -1,10 +1,11 @@ -import { ClerkContextProvider } from '@clerk/shared/react'; +import { ClerkContextProvider, ClerkInstanceContext } from '@clerk/shared/react'; import React from 'react'; +import { errorThrower } from '../errors/errorThrower'; import { multipleClerkProvidersError } from '../errors/messages'; import { IsomorphicClerk } from '../isomorphicClerk'; import type { ClerkProviderProps, IsomorphicClerkOptions, Ui } from '../types'; -import { mergeWithEnv, withMaxAllowedInstancesGuard } from '../utils'; +import { mergeWithEnv } from '../utils'; import { IS_REACT_SHARED_VARIANT_COMPATIBLE } from '../utils/versionCheck'; function ClerkProviderBase(props: ClerkProviderProps) { @@ -26,7 +27,14 @@ function ClerkProviderBase(props: ClerkProviderProps) { ); } -const ClerkProvider = withMaxAllowedInstancesGuard(ClerkProviderBase, 'ClerkProvider', multipleClerkProvidersError); +function ClerkProvider(props: ClerkProviderProps) { + // Context is per React tree, so a second root or React Native surface sharing + // the JS runtime can never false-positive as a nested provider. + if (React.useContext(ClerkInstanceContext)) { + errorThrower.throw(multipleClerkProvidersError); + } + return ; +} ClerkProvider.displayName = 'ClerkProvider'; diff --git a/packages/react/src/contexts/__tests__/ClerkProvider.nesting.test.tsx b/packages/react/src/contexts/__tests__/ClerkProvider.nesting.test.tsx new file mode 100644 index 00000000000..20cf1a95c56 --- /dev/null +++ b/packages/react/src/contexts/__tests__/ClerkProvider.nesting.test.tsx @@ -0,0 +1,65 @@ +import { render } from '@testing-library/react'; +import React from 'react'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; + +vi.mock('../../isomorphicClerk', () => { + let instance: any; + class IsomorphicClerk { + status = 'loading'; + on = vi.fn(); + off = vi.fn(); + __internal_updateProps = vi.fn().mockResolvedValue(undefined); + static getOrCreateInstance() { + instance ??= new IsomorphicClerk(); + return instance; + } + static clearInstance() { + instance = undefined; + } + } + return { IsomorphicClerk }; +}); + +import { ClerkProvider } from '../ClerkProvider'; + +const pk = 'pk_test_Y2xlcmsuY2xlcmsuZGV2JA'; + +const originalError = console.error; + +describe('ClerkProvider duplicate detection', () => { + beforeAll(() => { + console.error = vi.fn(); + }); + + afterAll(() => { + console.error = originalError; + }); + + it('throws when a ClerkProvider is nested inside another ClerkProvider', () => { + expect(() => + render( + + +
+ + , + ), + ).toThrow(/multiple /); + }); + + it('does not throw when a second React root mounts while the first is still mounted', () => { + const first = render( + +
+ , + ); + expect(() => + render( + +
+ , + ), + ).not.toThrow(); + first.unmount(); + }); +}); From 94509e02349c3ddbe3f7578168f7ec32d279e7ec Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 4 Aug 2026 20:33:04 -0700 Subject: [PATCH 2/3] move test --- .../__tests__/ClerkProvider.nesting.test.tsx | 65 ------------------- .../contexts/__tests__/ClerkProvider.test.tsx | 65 ++++++++++++++++++- 2 files changed, 63 insertions(+), 67 deletions(-) delete mode 100644 packages/react/src/contexts/__tests__/ClerkProvider.nesting.test.tsx diff --git a/packages/react/src/contexts/__tests__/ClerkProvider.nesting.test.tsx b/packages/react/src/contexts/__tests__/ClerkProvider.nesting.test.tsx deleted file mode 100644 index 20cf1a95c56..00000000000 --- a/packages/react/src/contexts/__tests__/ClerkProvider.nesting.test.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { render } from '@testing-library/react'; -import React from 'react'; -import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; - -vi.mock('../../isomorphicClerk', () => { - let instance: any; - class IsomorphicClerk { - status = 'loading'; - on = vi.fn(); - off = vi.fn(); - __internal_updateProps = vi.fn().mockResolvedValue(undefined); - static getOrCreateInstance() { - instance ??= new IsomorphicClerk(); - return instance; - } - static clearInstance() { - instance = undefined; - } - } - return { IsomorphicClerk }; -}); - -import { ClerkProvider } from '../ClerkProvider'; - -const pk = 'pk_test_Y2xlcmsuY2xlcmsuZGV2JA'; - -const originalError = console.error; - -describe('ClerkProvider duplicate detection', () => { - beforeAll(() => { - console.error = vi.fn(); - }); - - afterAll(() => { - console.error = originalError; - }); - - it('throws when a ClerkProvider is nested inside another ClerkProvider', () => { - expect(() => - render( - - -
- - , - ), - ).toThrow(/multiple /); - }); - - it('does not throw when a second React root mounts while the first is still mounted', () => { - const first = render( - -
- , - ); - expect(() => - render( - -
- , - ), - ).not.toThrow(); - first.unmount(); - }); -}); diff --git a/packages/react/src/contexts/__tests__/ClerkProvider.test.tsx b/packages/react/src/contexts/__tests__/ClerkProvider.test.tsx index 375d0236ee6..5fd2191b08e 100644 --- a/packages/react/src/contexts/__tests__/ClerkProvider.test.tsx +++ b/packages/react/src/contexts/__tests__/ClerkProvider.test.tsx @@ -16,10 +16,30 @@ import { ukUA, } from '@clerk/localizations'; import { dark } from '@clerk/ui/themes'; -import { describe, expectTypeOf, it } from 'vitest'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { afterAll, beforeAll, describe, expect, expectTypeOf, it, vi } from 'vitest'; import type { ClerkProviderProps as GenericClerkProviderProps, Ui } from '../../types'; -import type { ClerkProvider } from '../ClerkProvider'; +import { ClerkProvider } from '../ClerkProvider'; + +vi.mock('../../isomorphicClerk', () => { + let instance: any; + class IsomorphicClerk { + status = 'loading'; + on = vi.fn(); + off = vi.fn(); + __internal_updateProps = vi.fn().mockResolvedValue(undefined); + static getOrCreateInstance() { + instance ??= new IsomorphicClerk(); + return instance; + } + static clearInstance() { + instance = undefined; + } + } + return { IsomorphicClerk }; +}); type ClerkProviderProps = Parameters[0]; type CustomAppearance = { @@ -232,4 +252,45 @@ describe('ClerkProvider', () => { }).toMatchTypeOf(); }); }); + + describe('duplicate detection', () => { + const pk = 'pk_test_Y2xlcmsuY2xlcmsuZGV2JA'; + const originalError = console.error; + + beforeAll(() => { + console.error = vi.fn(); + }); + + afterAll(() => { + console.error = originalError; + }); + + it('throws when a ClerkProvider is nested inside another ClerkProvider', () => { + expect(() => + render( + + +
+ + , + ), + ).toThrow(/multiple /); + }); + + it('does not throw when a second React root mounts while the first is still mounted', () => { + const first = render( + +
+ , + ); + expect(() => + render( + +
+ , + ), + ).not.toThrow(); + first.unmount(); + }); + }); }); From 8efe61fd93c626db8b84768ed6a0158d0af9c251 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 4 Aug 2026 21:05:30 -0700 Subject: [PATCH 3/3] fix(react): Preserve ClerkProvider's published type shape --- packages/react/src/contexts/ClerkProvider.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/react/src/contexts/ClerkProvider.tsx b/packages/react/src/contexts/ClerkProvider.tsx index 739b7a571cc..fb0274520d6 100644 --- a/packages/react/src/contexts/ClerkProvider.tsx +++ b/packages/react/src/contexts/ClerkProvider.tsx @@ -27,7 +27,7 @@ function ClerkProviderBase(props: ClerkProviderProps) { ); } -function ClerkProvider(props: ClerkProviderProps) { +function ClerkProviderGuard(props: ClerkProviderProps) { // Context is per React tree, so a second root or React Native surface sharing // the JS runtime can never false-positive as a nested provider. if (React.useContext(ClerkInstanceContext)) { @@ -36,6 +36,9 @@ function ClerkProvider(props: ClerkProviderProps) { return ; } +// Cast preserves the pre-existing public type of ClerkProvider so the export is not a breaking change. +const ClerkProvider = ClerkProviderGuard as typeof ClerkProviderBase & { displayName: string }; + ClerkProvider.displayName = 'ClerkProvider'; export { ClerkProvider };