diff --git a/.changeset/calm-actors-wait.md b/.changeset/calm-actors-wait.md new file mode 100644 index 00000000000..91eb2559902 --- /dev/null +++ b/.changeset/calm-actors-wait.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +Fix a development-only React key warning when using `ClerkProvider` in the App Router. diff --git a/packages/nextjs/src/app-router/client/ClerkProvider.tsx b/packages/nextjs/src/app-router/client/ClerkProvider.tsx index fb6834585a4..d86ec291f8e 100644 --- a/packages/nextjs/src/app-router/client/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/client/ClerkProvider.tsx @@ -79,9 +79,9 @@ const NextClientClerkProvider = (props: NextClerkProviderPr return ( - - {__internal_scriptsSlot ?? } - {children} + + {__internal_scriptsSlot ?? } + {children} ); diff --git a/packages/nextjs/src/app-router/client/__tests__/ClerkProvider.test.tsx b/packages/nextjs/src/app-router/client/__tests__/ClerkProvider.test.tsx new file mode 100644 index 00000000000..63b4f2a65e0 --- /dev/null +++ b/packages/nextjs/src/app-router/client/__tests__/ClerkProvider.test.tsx @@ -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( + + content + , + ); + + 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( + scripts} + > + content + , + ); + + expect(childrenOf(capturedChildren).every(child => child.key !== null)).toBe(true); + }); +}); diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index b43d83306c8..ec995077429 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -38,7 +38,7 @@ export async function ClerkProvider( // 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 ? ( - + { + const actual = await vi.importActual('react'); + const cache = (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'); + }); +});