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/calm-actors-wait.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/nextjs': patch
---

Fix a development-only React key warning when using `ClerkProvider` in the App Router.
6 changes: 3 additions & 3 deletions packages/nextjs/src/app-router/client/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -79,9 +79,9 @@ const NextClientClerkProvider = <TUi extends Ui = Ui>(props: NextClerkProviderPr
return (
<ClerkNextOptionsProvider options={mergedProps}>
<ReactClerkProvider {...mergedProps}>
<RouterTelemetry />
{__internal_scriptsSlot ?? <ClerkScripts />}
{children}
<RouterTelemetry key='clerk-router-telemetry' />
{__internal_scriptsSlot ?? <ClerkScripts key='clerk-scripts' />}
<React.Fragment key='clerk-children'>{children}</React.Fragment>
</ReactClerkProvider>
</ClerkNextOptionsProvider>
);
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
import { cleanup, render } from '@testing-library/react';
import React from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

let capturedChildren: React.ReactNode;

vi.mock('@clerk/react/internal', () => ({
InternalClerkProvider: ({ children }: { children: React.ReactNode }) => {
capturedChildren = children;
return null;
},
}));

vi.mock('next/navigation', () => ({
useRouter: () => ({ refresh: vi.fn() }),
}));

vi.mock('../../server-actions', () => ({
invalidateCacheAction: vi.fn(),
}));

vi.mock('../useAwaitablePush', () => ({
useAwaitablePush: () => vi.fn(),
}));

vi.mock('../useAwaitableReplace', () => ({
useAwaitableReplace: () => vi.fn(),
}));

vi.mock('../../../utils/router-telemetry', () => ({
RouterTelemetry: () => null,
}));

vi.mock('../ClerkScripts', () => ({
ClerkScripts: () => null,
}));

import { ClientClerkProvider } from '../ClerkProvider';

const childrenOf = (node: React.ReactNode): React.ReactElement[] => {
if (!Array.isArray(node)) {
throw new Error('Expected the provider to receive an array of children');
}
return node as React.ReactElement[];
};

describe('ClientClerkProvider', () => {
beforeEach(() => {
capturedChildren = undefined;
});

afterEach(() => {
cleanup();
vi.clearAllMocks();
});

it('keys every child it passes to the React provider', () => {
render(
<ClientClerkProvider
publishableKey='pk_test_123'
disableKeyless
>
<span>content</span>
</ClientClerkProvider>,
);

expect(childrenOf(capturedChildren).map(child => child.key)).toEqual([
'clerk-router-telemetry',
'clerk-scripts',
'clerk-children',
]);
});

it('keeps the key of a scripts slot supplied by the server provider', () => {
render(
<ClientClerkProvider
publishableKey='pk_test_123'
disableKeyless
__internal_scriptsSlot={<span key='clerk-scripts'>scripts</span>}
>
<span>content</span>
</ClientClerkProvider>,
);

expect(childrenOf(capturedChildren).every(child => child.key !== null)).toBe(true);
});
});
2 changes: 1 addition & 1 deletion packages/nextjs/src/app-router/server/ClerkProvider.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,7 @@ export async function ClerkProvider<TUi extends Ui = Ui>(
// the nonce fetching (which calls headers()) from the rest of the page.
// This allows the page to remain statically renderable / use PPR.
const scriptsSlot = dynamic ? (
<Suspense>
<Suspense key='clerk-scripts'>
<DynamicClerkScripts
publishableKey={propsWithEnvs.publishableKey}
__internal_clerkJSUrl={propsWithEnvs.__internal_clerkJSUrl}
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
import type React from 'react';
import { describe, expect, it, vi } from 'vitest';

// The package's unit tests run against React 18, which has no `React.cache`.
vi.mock('react', async () => {
const actual = await vi.importActual<typeof import('react')>('react');
const cache = <T,>(fn: T) => fn;
return { ...actual, cache, default: { ...actual, cache } };
});

vi.mock('../keyless-provider', () => ({
getKeylessStatus: vi.fn().mockResolvedValue({ shouldRunAsKeyless: false, runningWithClaimedKeys: false }),
KeylessProvider: () => null,
}));

vi.mock('../../client/ClerkProvider', () => ({
ClientClerkProvider: () => null,
}));

vi.mock('../utils', () => ({
buildRequestLike: vi.fn().mockResolvedValue({}),
}));

vi.mock('../../../server/buildClerkProps', () => ({
getDynamicAuthData: vi.fn().mockReturnValue({}),
}));

import { ClerkProvider } from '../ClerkProvider';

describe('ClerkProvider', () => {
it('keys the dynamic scripts slot it passes to the client provider', async () => {
const element = (await ClerkProvider({
publishableKey: 'pk_test_123',
dynamic: true,
children: null,
})) as React.ReactElement<{ __internal_scriptsSlot: React.ReactElement }>;

expect(element.props.__internal_scriptsSlot.key).toBe('clerk-scripts');
});
});