Preliminary Checks
Reproduction
https://github.com/ant1m4tt3r/clerk-dynamic-key-warning
Publishable key
pk_test_ZGVzdGluZWQtYXBlLTQwLmNsZXJrLmFjY291bnRzLmRldiQ
Description
Hi! This issue started popping on recently in my app and I dug in a little to find out what caused it.
TLDR: a console warning shows in development caused by a React dev-only key check that misfires on Clerk's provider when RSC streams its children as separate chunks. The reported behavior also does not seem to cause any bugs, just a minor discomfort in development as it appears to be an application side bug.
The content below was generated by AI, but I've verified everything that was written and it appears to be correct.
Rendering <ClerkProvider> from a Server Component root layout with a large prop (the documented localization prop) makes React log this on every page load:
Each child in a list should have a unique "key" prop.
Check the render method of __experimental_CheckoutProvider. It was passed a child from ClerkProvider.
Nothing in the app renders a list — the array is created inside Clerk.
Steps to reproduce:
- Clone the reproduction,
npm install, put a pk_test_... in .env.local, npm run dev
- Open http://localhost:3000 and look at the browser console → the warning is logged
- In
app/layout.tsx, swap localization={ptBR} for a small object such as {{ signIn: { start: { title: "Hi" } } }}, reload → the console is clean
No clerkMiddleware and no secret key are involved; the warning is independent of auth state. The React Compiler is not required.
Expected behavior: no React key warnings from Clerk internals.
Actual behavior: the warning above on every page load, pointing at a list the application does not have.
Root cause
NextClientClerkProvider (app-router/client/ClerkProvider.js) renders three static sibling children:
React.createElement(ReactClerkProvider, { ...mergedProps },
React.createElement(RouterTelemetry, null),
__internal_scriptsSlot != null ? __internal_scriptsSlot : React.createElement(ClerkScripts, null),
children
)
That array is forwarded as a single props.children through ClerkProviderBase → ClerkContextProvider → __experimental_CheckoutProvider (@clerk/shared/react), where React reconciles it as an array and key-checks each entry.
Static siblings never need keys, which is why this normally warns about nothing — React enforces that rule by marking each positional child as validated. What the prop size changes is whether that mark lands where the reconciler later looks for it. With a large prop, React outlines the children into separate flight rows — the same element, serialized both ways:
// small prop — one 1.5 KB row, children inlined as elements
"__internal_scriptsSlot": ["$","$36",null,{…},"$1c","$5a",0],
"children": ["$","$L5f",null,{…},null,"$5e",1]
// localization={ptBR} — one 74 KB row, children outlined
"__internal_scriptsSlot": "$L5a",
"children": "$L5b"
A $L<id> deserializes to a lazy node, not an element (the trailing field is React's validated flag — the scripts slot ships as 0). validateChildKeys in react.development.js can only mark a lazy that has already resolved:
isValidElement(node)
? node._store && (node._store.validated = 1) // inlined element → marked
: node.$$typeof === REACT_LAZY_TYPE &&
("fulfilled" === node._payload.status
? /* mark the resolved element */
: node._store && (node._store.validated = 1)); // pending → marks the WRAPPER
At createElement time the outlined rows have not arrived, so the mark lands on the wrapper. The reconciler later does case REACT_LAZY_TYPE: (child = resolveLazy(child)), warnOnInvalidKey(...), finds validated: 0 on the resolved scripts slot with no key, and warns — naming the reconciling parent (__experimental_CheckoutProvider) and the child's owner (ClerkProvider).
Suggested fix
Key the children where they are created, e.g. in app-router/server/ClerkProvider.js:
const scriptsSlot = dynamic ? (
<Suspense key="clerk-scripts"><DynamicClerkScripts … /></Suspense>
) : undefined;
and/or key the three children in NextClientClerkProvider. Keys make the array immune regardless of how the payload is chunked.
Version notes
Reproduced on @clerk/nextjs 7.7.4 (latest at time of filing). On 7.4.0 it additionally required the dynamic prop; on 7.7.4 the large prop alone is enough. Development-only (React strips key validation in production), but it fires on every page load and reads like an application bug.
Possible bug origin
The scripts slot and its sibling children were introduced in #7773 (Isolate nonce fetch in Suspense boundary for PPR support), which added <Suspense> around DynamicClerkScripts in the server provider and {__internal_scriptsSlot ?? <ClerkScripts />} as one of three unkeyed siblings in the client provider.
Environment
System:
OS: macOS 26.5
CPU: (12) arm64 Apple M4 Pro
Memory: 246.88 MB / 24.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 20.19.6 - /Users/antimatter/.nvm/versions/node/v20.19.6/bin/node
Yarn: 3.2.4 - /Users/antimatter/.nvm/versions/node/v20.19.6/bin/yarn
npm: 10.8.2 - /Users/antimatter/.nvm/versions/node/v20.19.6/bin/npm
pnpm: 8.10.5 - /Users/antimatter/.nvm/versions/node/v20.19.6/bin/pnpm
bun: 1.2.19 - /Users/antimatter/.bun/bin/bun
Deno: 2.1.3 - /opt/homebrew/bin/deno
Browsers:
Chrome: 151.0.7922.109
Firefox: 125.0.3
Safari: 26.5
npmPackages:
@clerk/localizations: ^4.15.1 => 4.15.1
@clerk/nextjs: ^7.6.4 => 7.7.4
@types/node: ^20 => 20.19.43
@types/react: ^19 => 19.2.18
@types/react-dom: ^19 => 19.2.4
next: 16.3.0 => 16.3.0
react: 19.2.6 => 19.2.6
react-dom: 19.2.6 => 19.2.6
typescript: ^5 => 5.9.3
Preliminary Checks
I have reviewed the documentation: https://clerk.com/docs
I have searched for existing issues: https://github.com/clerk/javascript/issues
I have not already reached out to Clerk support via email or Discord (if you have, no need to open an issue here)
This issue is not a question, general help request, or anything other than a bug report directly related to Clerk. Please ask questions in our Discord community: https://clerk.com/discord.
Reproduction
https://github.com/ant1m4tt3r/clerk-dynamic-key-warning
Publishable key
pk_test_ZGVzdGluZWQtYXBlLTQwLmNsZXJrLmFjY291bnRzLmRldiQ
Description
Hi! This issue started popping on recently in my app and I dug in a little to find out what caused it.
TLDR: a console warning shows in development caused by a React dev-only key check that misfires on Clerk's provider when RSC streams its children as separate chunks. The reported behavior also does not seem to cause any bugs, just a minor discomfort in development as it appears to be an application side bug.
The content below was generated by AI, but I've verified everything that was written and it appears to be correct.
Rendering
<ClerkProvider>from a Server Component root layout with a large prop (the documentedlocalizationprop) makes React log this on every page load:Nothing in the app renders a list — the array is created inside Clerk.
Steps to reproduce:
npm install, put apk_test_...in.env.local,npm run devapp/layout.tsx, swaplocalization={ptBR}for a small object such as{{ signIn: { start: { title: "Hi" } } }}, reload → the console is cleanNo
clerkMiddlewareand no secret key are involved; the warning is independent of auth state. The React Compiler is not required.Expected behavior: no React key warnings from Clerk internals.
Actual behavior: the warning above on every page load, pointing at a list the application does not have.
Root cause
NextClientClerkProvider(app-router/client/ClerkProvider.js) renders three static sibling children:That array is forwarded as a single
props.childrenthroughClerkProviderBase→ClerkContextProvider→__experimental_CheckoutProvider(@clerk/shared/react), where React reconciles it as an array and key-checks each entry.Static siblings never need keys, which is why this normally warns about nothing — React enforces that rule by marking each positional child as validated. What the prop size changes is whether that mark lands where the reconciler later looks for it. With a large prop, React outlines the children into separate flight rows — the same element, serialized both ways:
A
$L<id>deserializes to a lazy node, not an element (the trailing field is React'svalidatedflag — the scripts slot ships as0).validateChildKeysinreact.development.jscan only mark a lazy that has already resolved:At
createElementtime the outlined rows have not arrived, so the mark lands on the wrapper. The reconciler later doescase REACT_LAZY_TYPE: (child = resolveLazy(child)), warnOnInvalidKey(...), findsvalidated: 0on the resolved scripts slot with no key, and warns — naming the reconciling parent (__experimental_CheckoutProvider) and the child's owner (ClerkProvider).Suggested fix
Key the children where they are created, e.g. in
app-router/server/ClerkProvider.js:and/or key the three children in
NextClientClerkProvider. Keys make the array immune regardless of how the payload is chunked.Version notes
Reproduced on
@clerk/nextjs7.7.4 (latest at time of filing). On 7.4.0 it additionally required thedynamicprop; on 7.7.4 the large prop alone is enough. Development-only (React strips key validation in production), but it fires on every page load and reads like an application bug.Possible bug origin
The scripts slot and its sibling children were introduced in #7773 (
Isolate nonce fetch in Suspense boundary for PPR support), which added<Suspense>aroundDynamicClerkScriptsin the server provider and{__internal_scriptsSlot ?? <ClerkScripts />}as one of three unkeyed siblings in the client provider.Environment
System: OS: macOS 26.5 CPU: (12) arm64 Apple M4 Pro Memory: 246.88 MB / 24.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 20.19.6 - /Users/antimatter/.nvm/versions/node/v20.19.6/bin/node Yarn: 3.2.4 - /Users/antimatter/.nvm/versions/node/v20.19.6/bin/yarn npm: 10.8.2 - /Users/antimatter/.nvm/versions/node/v20.19.6/bin/npm pnpm: 8.10.5 - /Users/antimatter/.nvm/versions/node/v20.19.6/bin/pnpm bun: 1.2.19 - /Users/antimatter/.bun/bin/bun Deno: 2.1.3 - /opt/homebrew/bin/deno Browsers: Chrome: 151.0.7922.109 Firefox: 125.0.3 Safari: 26.5 npmPackages: @clerk/localizations: ^4.15.1 => 4.15.1 @clerk/nextjs: ^7.6.4 => 7.7.4 @types/node: ^20 => 20.19.43 @types/react: ^19 => 19.2.18 @types/react-dom: ^19 => 19.2.4 next: 16.3.0 => 16.3.0 react: 19.2.6 => 19.2.6 react-dom: 19.2.6 => 19.2.6 typescript: ^5 => 5.9.3