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
5 changes: 5 additions & 0 deletions .changeset/spotty-planes-nest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/react': patch
---

Fix a false-positive "multiple `<ClerkProvider>`" crash in apps that run more than one React root in a single JavaScript runtime, most commonly React Native Android apps during activity recreation. `<ClerkProvider>` now throws this error only when it is genuinely nested inside another `<ClerkProvider>`.
17 changes: 14 additions & 3 deletions packages/react/src/contexts/ClerkProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<TUi extends Ui>(props: ClerkProviderProps<TUi>) {
Expand All @@ -26,7 +27,17 @@ function ClerkProviderBase<TUi extends Ui>(props: ClerkProviderProps<TUi>) {
);
}

const ClerkProvider = withMaxAllowedInstancesGuard(ClerkProviderBase, 'ClerkProvider', multipleClerkProvidersError);
function ClerkProviderGuard<TUi extends Ui>(props: ClerkProviderProps<TUi>) {
// 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 <ClerkProviderBase {...props} />;
}

// 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';

Expand Down
65 changes: 63 additions & 2 deletions packages/react/src/contexts/__tests__/ClerkProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof ClerkProvider>[0];
type CustomAppearance = {
Expand Down Expand Up @@ -232,4 +252,45 @@ describe('ClerkProvider', () => {
}).toMatchTypeOf<ClerkProviderProps>();
});
});

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(
<ClerkProvider publishableKey={pk}>
<ClerkProvider publishableKey={pk}>
<div />
</ClerkProvider>
</ClerkProvider>,
),
).toThrow(/multiple <ClerkProvider>/);
});

it('does not throw when a second React root mounts while the first is still mounted', () => {
const first = render(
<ClerkProvider publishableKey={pk}>
<div />
</ClerkProvider>,
);
expect(() =>
render(
<ClerkProvider publishableKey={pk}>
<div />
</ClerkProvider>,
),
).not.toThrow();
first.unmount();
});
});
});
Loading