diff --git a/.changeset/gorgeous-countries-appear.md b/.changeset/gorgeous-countries-appear.md
new file mode 100644
index 00000000000..9720c566af9
--- /dev/null
+++ b/.changeset/gorgeous-countries-appear.md
@@ -0,0 +1,5 @@
+---
+'@clerk/nextjs': patch
+---
+
+Update `` to work in client components within the app router. This allows rendering of the provider in client components, previously the pages router provider was being imported and throwing an error.
diff --git a/integration/models/application.ts b/integration/models/application.ts
index f2f080f30d0..992cfd92b02 100644
--- a/integration/models/application.ts
+++ b/integration/models/application.ts
@@ -16,6 +16,7 @@ export const application = (config: ApplicationConfig, appDirPath: string, appDi
const now = Date.now();
const stdoutFilePath = path.resolve(appDirPath, `e2e.${now}.log`);
const stderrFilePath = path.resolve(appDirPath, `e2e.${now}.err.log`);
+ let buildOutput = '';
const self = {
name,
@@ -70,7 +71,16 @@ export const application = (config: ApplicationConfig, appDirPath: string, appDi
},
build: async () => {
const log = logger.child({ prefix: 'build' }).info;
- await run(scripts.build, { cwd: appDirPath, log });
+ await run(scripts.build, {
+ cwd: appDirPath,
+ log: (msg: string) => {
+ buildOutput += `\n${msg}`;
+ log(msg);
+ },
+ });
+ },
+ get buildOutput() {
+ return buildOutput;
},
serve: async (opts: { port?: number; manualStart?: boolean } = {}) => {
const port = opts.port || (await getPort());
diff --git a/integration/tests/global.teardown.ts b/integration/tests/global.teardown.ts
index 19fde3e2f1d..a2acdfddf25 100644
--- a/integration/tests/global.teardown.ts
+++ b/integration/tests/global.teardown.ts
@@ -12,11 +12,13 @@ setup('teardown long running apps', async () => {
console.log('Skipping cleanup');
return;
}
- const runningAppsFoundInStateFile = Object.values(stateFile.getLongRunningApps());
- const matchingLongRunningApps = appConfigs.longRunningApps.getByPattern(
- runningAppsFoundInStateFile.map(app => app.id),
- );
- await Promise.all(matchingLongRunningApps.map(app => app.destroy()));
+ const runningAppsFoundInStateFile = Object.values(stateFile.getLongRunningApps() ?? {});
+ if (runningAppsFoundInStateFile.length > 0) {
+ const matchingLongRunningApps = appConfigs.longRunningApps.getByPattern(
+ runningAppsFoundInStateFile.map(app => app.id),
+ );
+ await Promise.all(matchingLongRunningApps.map(app => app.destroy()));
+ }
stateFile.remove();
console.log('Long running apps destroyed');
});
diff --git a/integration/tests/next-build.test.ts b/integration/tests/next-build.test.ts
new file mode 100644
index 00000000000..7f5cf9394e0
--- /dev/null
+++ b/integration/tests/next-build.test.ts
@@ -0,0 +1,72 @@
+import { expect, test } from '@playwright/test';
+
+import type { Application } from '../models/application';
+import { appConfigs } from '../presets';
+
+test.describe('next build @nextjs', () => {
+ test.describe.configure({ mode: 'parallel' });
+ let app: Application;
+ const output = [];
+
+ test.beforeAll(async () => {
+ app = await appConfigs.next.appRouter
+ .clone()
+ .addFile(
+ 'src/app/provider.tsx',
+ () => `'use client'
+import { ClerkProvider } from "@clerk/nextjs"
+
+export function Provider({ children }: { children: any }) {
+ return (
+
+ {children}
+
+ )
+}`,
+ )
+ .addFile(
+ 'src/app/layout.tsx',
+ () => `import './globals.css';
+import { Inter } from 'next/font/google';
+import { Provider } from './provider';
+
+const inter = Inter({ subsets: ['latin'] });
+
+export const metadata = {
+ title: 'Create Next App',
+ description: 'Generated by create next app',
+};
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+ `,
+ )
+ .commit();
+ await app.setup();
+ await app.withEnv(appConfigs.envs.withEmailCodes);
+ await app.build();
+ });
+
+ test.afterAll(async () => {
+ await app.teardown();
+ });
+
+ test('When is used as a client component, builds successfully and does not force dynamic rendering', () => {
+ const dynamicIndicator = 'λ';
+
+ /**
+ * Using /_not-found as it is an internal page that should statically render by default.
+ * This is a good indicator of whether or not the entire app has been opted-in to dynamic rendering.
+ */
+ const notFoundPageLine = app.buildOutput.split('\n').find(msg => msg.includes('/_not-found'));
+
+ expect(notFoundPageLine).not.toContain(dynamicIndicator);
+ });
+});
diff --git a/package.json b/package.json
index 740c609adda..2d59e859300 100644
--- a/package.json
+++ b/package.json
@@ -69,7 +69,7 @@
"test:ci": "FORCE_COLOR=1 turbo test --concurrency=${TURBO_CONCURRENCY:-80%}",
"test:integration:base": "DEBUG=1 npx playwright test --config integration/playwright.config.ts",
"test:integration:generic": "E2E_APP_ID=react.vite.* npm run test:integration:base -- --grep @generic",
- "test:integration:nextjs": "E2E_APP_ID=next.appRouter.withEmailCodes npm run test:integration:base -- --grep @generic",
+ "test:integration:nextjs": "E2E_APP_ID=next.appRouter.withEmailCodes npm run test:integration:base -- --grep \"@generic|@nextjs\"",
"test:integration:remix": "echo 'placeholder'",
"test:integration:deployment:nextjs": "DEBUG=1 npx playwright test --config integration/playwright.deployments.config.ts",
"clean": "turbo clean",
diff --git a/packages/nextjs/src/app-router/client/ClerkProvider.tsx b/packages/nextjs/src/app-router/client/ClerkProvider.tsx
index 7bcdee4a91a..8fe3ca193ab 100644
--- a/packages/nextjs/src/app-router/client/ClerkProvider.tsx
+++ b/packages/nextjs/src/app-router/client/ClerkProvider.tsx
@@ -6,6 +6,7 @@ import React from 'react';
import { ClerkNextOptionsProvider } from '../../client-boundary/NextOptionsContext';
import { useSafeLayoutEffect } from '../../client-boundary/useSafeLayoutEffect';
import type { NextClerkProviderProps } from '../../types';
+import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv';
import { useAwaitableNavigate } from './useAwaitableNavigate';
declare global {
@@ -33,7 +34,7 @@ export const ClientClerkProvider = (props: NextClerkProviderProps) => {
};
}, []);
- const mergedProps = { ...props, navigate };
+ const mergedProps = mergeNextClerkPropsWithEnv({ ...props, navigate });
return (
{/*// @ts-ignore*/}
diff --git a/packages/nextjs/src/client-boundary/ClerkProvider.tsx b/packages/nextjs/src/client-boundary/ClerkProvider.tsx
new file mode 100644
index 00000000000..3624aefd6e1
--- /dev/null
+++ b/packages/nextjs/src/client-boundary/ClerkProvider.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import { useRouter } from 'next/compat/router';
+import React from 'react';
+
+import { ClientClerkProvider } from '../app-router/client/ClerkProvider';
+import { ClerkProvider as PageClerkProvider } from '../pages/ClerkProvider';
+import { type NextClerkProviderProps } from '../types';
+
+/**
+ * This is a compatibility layer to support a single ClerkProvider component in both the app and pages routers.
+ */
+export function ClerkProvider(props: NextClerkProviderProps) {
+ const router = useRouter();
+
+ const Provider = router ? PageClerkProvider : ClientClerkProvider;
+
+ return ;
+}
diff --git a/packages/nextjs/src/components.client.ts b/packages/nextjs/src/components.client.ts
index 6c41451e6d7..2fcf82aa99c 100644
--- a/packages/nextjs/src/components.client.ts
+++ b/packages/nextjs/src/components.client.ts
@@ -1,2 +1,2 @@
-export { ClerkProvider } from './pages/ClerkProvider';
+export { ClerkProvider } from './client-boundary/ClerkProvider';
export { SignedIn, SignedOut } from './client-boundary/controlComponents';