diff --git a/.changeset/shared-context-singleton.md b/.changeset/shared-context-singleton.md
new file mode 100644
index 00000000000..4d6a82f002a
--- /dev/null
+++ b/.changeset/shared-context-singleton.md
@@ -0,0 +1,5 @@
+---
+'@clerk/shared': patch
+---
+
+Make React contexts created by `createContextAndHook` resilient to duplicate module instances. When a bundler ends up with more than one copy of `@clerk/shared/react` (for example Vite/Rolldown pre-bundling it separately from the `ClerkProvider`), each copy previously created its own context object, so a provider from one copy and a `useClerk`/`use*Context` hook from another would not match and threw "... not found". Contexts are now cached on a global registry keyed by package version and display name, so every duplicated instance shares one context object.
diff --git a/packages/shared/src/react/__tests__/createContextAndHook.test.tsx b/packages/shared/src/react/__tests__/createContextAndHook.test.tsx
new file mode 100644
index 00000000000..d98f4097a50
--- /dev/null
+++ b/packages/shared/src/react/__tests__/createContextAndHook.test.tsx
@@ -0,0 +1,58 @@
+import { render, screen } from '@testing-library/react';
+import React from 'react';
+import { afterEach, describe, expect, it } from 'vitest';
+
+import { createContextAndHook } from '../hooks/createContextAndHook';
+
+afterEach(() => {
+ // Reset the cross-instance registry between tests.
+ globalThis.__clerk_shared_react_contexts = undefined;
+});
+
+describe('createContextAndHook', () => {
+ it('returns the same context object for repeated calls with the same display name', () => {
+ const [CtxA] = createContextAndHook<{ label: string }>('SharedContext');
+ const [CtxB] = createContextAndHook<{ label: string }>('SharedContext');
+
+ // A duplicated module instance would call createContextAndHook again; both must
+ // resolve to one context object so provider/consumer identity matches.
+ expect(CtxA).toBe(CtxB);
+ });
+
+ it('returns different context objects for different display names', () => {
+ const [CtxA] = createContextAndHook<{ label: string }>('ContextOne');
+ const [CtxB] = createContextAndHook<{ label: string }>('ContextTwo');
+
+ expect(CtxA).not.toBe(CtxB);
+ });
+
+ it('lets a provider from one instance satisfy a hook from a duplicate instance', () => {
+ // Simulate two separately-bundled copies of this module by calling the factory twice.
+ const [ProviderCtx] = createContextAndHook<{ label: string }>('DuplicatedContext');
+ const [, useDuplicatedContext] = createContextAndHook<{ label: string }>('DuplicatedContext');
+
+ const Consumer = () => {
+ const { label } = useDuplicatedContext();
+ return
{label}
;
+ };
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId('value').textContent).toBe('from-provider');
+ });
+
+ it('still throws when the hook is used without a provider', () => {
+ const [, useMissingContext] = createContextAndHook<{ label: string }>('MissingContext');
+
+ const Consumer = () => {
+ useMissingContext();
+ return null;
+ };
+
+ expect(() => render()).toThrow(/MissingContext not found/);
+ });
+});
diff --git a/packages/shared/src/react/hooks/createContextAndHook.ts b/packages/shared/src/react/hooks/createContextAndHook.ts
index 390ba4baf50..8e073c7b000 100644
--- a/packages/shared/src/react/hooks/createContextAndHook.ts
+++ b/packages/shared/src/react/hooks/createContextAndHook.ts
@@ -16,6 +16,40 @@ type Options = { assertCtxFn?: (v: unknown, msg: string) => void };
type ContextOf = React.Context<{ value: T } | undefined>;
type UseCtxFn = () => T;
+declare global {
+ var __clerk_shared_react_contexts: Map> | undefined;
+}
+
+/**
+ * React matches a provider to a consumer by the identity of the context object returned
+ * from `React.createContext`. When a bundler ends up with more than one instance of this
+ * module (for example Vite/Rolldown pre-bundling `@clerk/shared/react` separately from the
+ * `ClerkProvider`), each instance would call `createContext` again and produce a *different*
+ * context object, so a provider from one instance and a `useClerk`/`use*Context` from another
+ * would never match and the assert hook throws "... not found".
+ *
+ * Cache each context on a global registry, keyed by package version + display name, so every
+ * duplicated instance of this module shares one context object. Keying by version keeps two
+ * legitimately different major versions of `@clerk/shared` from colliding on the same key.
+ */
+const getOrCreateContext = (displayName: string): ContextOf => {
+ const registry = (globalThis.__clerk_shared_react_contexts ??= new Map());
+ const key = `${PACKAGE_VERSION}:${displayName}`;
+
+ const existing = registry.get(key);
+ if (existing) {
+ // SAFETY: the registry is keyed by version + displayName, and each key is only ever
+ // written by the createContextAndHook(displayName) call that owns that name, so
+ // the stored context always carries the CtxVal this caller expects.
+ return existing as ContextOf;
+ }
+
+ const Ctx = React.createContext<{ value: CtxVal } | undefined>(undefined);
+ Ctx.displayName = displayName;
+ registry.set(key, Ctx as React.Context<{ value: unknown } | undefined>);
+ return Ctx;
+};
+
/**
* Create and return a Context and two hooks that return the context value.
* The Context type is derived from the type passed in by the user.
@@ -30,8 +64,7 @@ export const createContextAndHook = (
options?: Options,
): [ContextOf, UseCtxFn, UseCtxFn>] => {
const { assertCtxFn = assertContextExists } = options || {};
- const Ctx = React.createContext<{ value: CtxVal } | undefined>(undefined);
- Ctx.displayName = displayName;
+ const Ctx = getOrCreateContext(displayName);
const useCtx = () => {
const ctx = React.useContext(Ctx);