Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
20 changes: 0 additions & 20 deletions packages/nextjs/src/app-router/client/ClerkProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { ClerkProvider as ReactClerkProvider } from '@clerk/react';
import type { Ui } from '@clerk/react/internal';
import { InitialStateProvider } from '@clerk/shared/react';
import type { ClerkUIConstructor } from '@clerk/shared/ui';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import React from 'react';
Expand All @@ -18,11 +17,6 @@ import { invalidateCacheAction } from '../server-actions';
import { useAwaitablePush } from './useAwaitablePush';
import { useAwaitableReplace } from './useAwaitableReplace';

// Cached promise for resolving ClerkUI constructor via dynamic import.
// In RSC, the ui prop from @clerk/ui is serialized without the ClerkUI constructor
// (not serializable). This re-imports it on the client when needed.
let _resolvedClerkUI: Promise<ClerkUIConstructor> | undefined;

/**
* LazyCreateKeylessApplication should only be loaded if the conditions below are met.
* Note: Using lazy() with Suspense instead of dynamic is not possible as React will throw a hydration error when `ClerkProvider` wraps `<html><body>...`
Expand Down Expand Up @@ -91,20 +85,6 @@ const NextClientClerkProvider = <TUi extends Ui = Ui>(props: NextClerkProviderPr
routerReplace: replace,
});

// Resolve ClerkUI for RSC: when the ui prop is serialized through React Server Components,
// the ClerkUI constructor is stripped (not serializable). Re-import it on the client.
const uiProp = mergedProps.ui as { __brand?: string; ClerkUI?: unknown } | undefined;
if (uiProp?.__brand && !uiProp?.ClerkUI) {
// webpackIgnore/turbopackIgnore prevent the bundler from statically resolving @clerk/ui/entry at build time,
// since @clerk/ui is an optional dependency that may not be installed.
// @ts-expect-error - @clerk/ui is an optional peer dependency, not declared in this package's dependencies
// eslint-disable-next-line import/no-unresolved
_resolvedClerkUI ??= import(/* webpackIgnore: true */ /* turbopackIgnore: true */ '@clerk/ui/entry').then(
(m: { ClerkUI: ClerkUIConstructor }) => m.ClerkUI,
);
mergedProps.ui = { ...mergedProps.ui, ClerkUI: _resolvedClerkUI };
}

return (
<ClerkNextOptionsProvider options={mergedProps}>
<ReactClerkProvider {...mergedProps}>
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/ClerkUI.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import { ClerkRuntimeError } from '@clerk/shared/error';
import { logger } from '@clerk/shared/logger';
import type { ModuleManager } from '@clerk/shared/moduleManager';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ exports[`module exports > default export (index.ts) > should have the expected s

exports[`module exports > server export (server.ts) > should have the expected shape 1`] = `
[
"ClerkUI",
"__brand",
"version",
]
Expand Down
5 changes: 3 additions & 2 deletions packages/ui/src/__tests__/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ describe('module exports', () => {
expect((serverUi as any).__brand).toBe('__clerkUI');
});

it('should NOT include ClerkUI constructor', () => {
expect((serverUi as any).ClerkUI).toBeUndefined();
it('should include ClerkUI constructor for RSC client reference', () => {
expect((serverUi as any).ClerkUI).toBeDefined();
expect(typeof (serverUi as any).ClerkUI).toBe('function');
});

it('should include version', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/entry.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
'use client';

export { ClerkUI } from './ClerkUI';
10 changes: 7 additions & 3 deletions packages/ui/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import type { Ui } from './internal';
import { UI_BRAND } from './internal';
import type { Appearance } from './internal/appearance';

import { ClerkUI } from './entry';

declare const PACKAGE_VERSION: string;

/**
* Server-safe UI marker for React Server Components.
* UI object for React Server Components.
*
* This export does not include the ClerkUI constructor, making it safe to import
* in server components. The constructor is resolved via dynamic import when needed.
* ClerkUI is imported from a 'use client' module so that RSC serializes it as a
* client reference. The bundler includes the actual ClerkUI code only in the
* client bundle and resolves the reference automatically on hydration.
*
* @example
* ```tsx
Expand All @@ -24,4 +27,5 @@ declare const PACKAGE_VERSION: string;
export const ui = {
__brand: UI_BRAND,
version: PACKAGE_VERSION,
ClerkUI,
} as unknown as Ui<Appearance>;
Loading