What you see
Running pnpm dev and opening the storefront, the browser console shows a React hydration mismatch warning pointing at <link> tags rendered by React Router's <Links> (e.g. /@react-router/critical.css and your theme CSS):
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties...
<link
+ nonce={undefined} (client)
- nonce="" (server)
rel="stylesheet"
href="/src/.../theme/index.css"
>
ELI5
When a page loads, the server writes the HTML first, then React "wakes it up" in the browser (hydration). During hydration React double-checks that what the browser built matches what the server sent. Here, one attribute disagrees: the server put a nonce (a one-time security token used by Content-Security-Policy) on the stylesheet <link>, but the browser side has no nonce, so React logs a warning.
Think of it like the server writing a name on a sticker and the browser expecting a blank sticker — React notices the two don't match and says so. It does not rip the sticker off and redo it.
It's only an attribute mismatch — this matters
React treats two kinds of mismatch very differently:
- Content/structure mismatch (different text, different tags) → React throws away the server HTML and re-renders on the client. This is expensive and can cause a visible flash.
- Attribute mismatch (our case: just the
nonce value) → React keeps the server-rendered DOM as-is and does not re-render. The warning even says so: "This won't be patched up."
We are firmly in the second category. Nothing is re-rendered, nothing flashes, the stylesheet still loads and applies normally.
Root cause
React Router 7.18.0 (PR #15170) made <Links> fall back to a nonce carried in React Router's framework context when no explicit nonce prop is passed:
if (nonce == null && contextNonce) nonce = contextNonce;
The asymmetry that produces the warning:
- Server:
<ServerRouter nonce> puts the nonce into the framework context, so <Links> stamps nonce="..." on every <link>.
- Client:
HydratedRouter does not include a nonce field in its framework context, so <Links> renders no nonce (undefined).
Server says "has nonce", client says "no nonce" → mismatch.
Why <script> tags don't warn but <link> tags do
This exact server-has-nonce / client-has-no-nonce asymmetry applies to React Router's <Scripts> output too — but <Scripts> never produces a visible warning. The difference is not the nonce; it's that React Router explicitly marks its script elements with React's suppressHydrationWarning flag, which tells React "don't diff the attributes on this node during hydration." So the same nonce mismatch is silently ignored on scripts.
<Links> does not set that flag on the <link> elements it renders, so React diffs them normally and logs the mismatch. React itself also special-cases <script>/<style> nonce handling internally, but treats <link> as a plain element — another reason links surface the warning and scripts don't.
Crucially, suppressHydrationWarning is set inside React Router's own components — it is not a prop we can pass to <Links> from application code. So we can't silence it from the template; the fix has to come from React Router (mark <Links> the same way <Scripts> is, or serialize the nonce into the client framework context so both sides match).
Confirmed still present in React Router 7.18.1 and 8.1.0 — not yet fixed upstream.
Impact
None in production. Cosmetic dev-only console noise.
- React only emits hydration warnings in development builds — production builds strip them, so shoppers never see anything in the console.
- Because it's an attribute-only mismatch, there is no re-render, no layout shift, no performance cost.
- A
nonce on an external <link rel=stylesheet> has no security function anyway — CSP's style-src authorizes external stylesheets by their source URL, not by nonce. So the "missing" client-side nonce changes nothing about CSP enforcement. Nonces that do matter (inline scripts) are unaffected.
No action is required in your storefront. This issue is for tracking/visibility until the upstream React Router fix lands.
Why React Router was upgraded in the first place
The framework upgraded React Router from 7.12 to 7.18 to pick up security fixes — most notably an arbitrary-code-execution advisory and several denial-of-service advisories in React Router's vendored turbo-stream / single-fetch handling. Staying on the older line would leave those unpatched, so the upgrade was the right call; this benign dev warning is a minor side effect of it.
Workaround
None needed. Safe to ignore the warning in dev. It disappears entirely in production builds (pnpm build + preview/deploy).
What you see
Running
pnpm devand opening the storefront, the browser console shows a React hydration mismatch warning pointing at<link>tags rendered by React Router's<Links>(e.g./@react-router/critical.cssand your theme CSS):ELI5
When a page loads, the server writes the HTML first, then React "wakes it up" in the browser (hydration). During hydration React double-checks that what the browser built matches what the server sent. Here, one attribute disagrees: the server put a
nonce(a one-time security token used by Content-Security-Policy) on the stylesheet<link>, but the browser side has no nonce, so React logs a warning.Think of it like the server writing a name on a sticker and the browser expecting a blank sticker — React notices the two don't match and says so. It does not rip the sticker off and redo it.
It's only an attribute mismatch — this matters
React treats two kinds of mismatch very differently:
noncevalue) → React keeps the server-rendered DOM as-is and does not re-render. The warning even says so: "This won't be patched up."We are firmly in the second category. Nothing is re-rendered, nothing flashes, the stylesheet still loads and applies normally.
Root cause
React Router 7.18.0 (PR #15170) made
<Links>fall back to a nonce carried in React Router's framework context when no explicitnonceprop is passed:The asymmetry that produces the warning:
<ServerRouter nonce>puts the nonce into the framework context, so<Links>stampsnonce="..."on every<link>.HydratedRouterdoes not include anoncefield in its framework context, so<Links>renders no nonce (undefined).Server says "has nonce", client says "no nonce" → mismatch.
Why
<script>tags don't warn but<link>tags doThis exact server-has-nonce / client-has-no-nonce asymmetry applies to React Router's
<Scripts>output too — but<Scripts>never produces a visible warning. The difference is not the nonce; it's that React Router explicitly marks its script elements with React'ssuppressHydrationWarningflag, which tells React "don't diff the attributes on this node during hydration." So the same nonce mismatch is silently ignored on scripts.<Links>does not set that flag on the<link>elements it renders, so React diffs them normally and logs the mismatch. React itself also special-cases<script>/<style>nonce handling internally, but treats<link>as a plain element — another reason links surface the warning and scripts don't.Crucially,
suppressHydrationWarningis set inside React Router's own components — it is not a prop we can pass to<Links>from application code. So we can't silence it from the template; the fix has to come from React Router (mark<Links>the same way<Scripts>is, or serialize the nonce into the client framework context so both sides match).Confirmed still present in React Router 7.18.1 and 8.1.0 — not yet fixed upstream.
Impact
None in production. Cosmetic dev-only console noise.
nonceon an external<link rel=stylesheet>has no security function anyway — CSP'sstyle-srcauthorizes external stylesheets by their source URL, not by nonce. So the "missing" client-side nonce changes nothing about CSP enforcement. Nonces that do matter (inline scripts) are unaffected.No action is required in your storefront. This issue is for tracking/visibility until the upstream React Router fix lands.
Why React Router was upgraded in the first place
The framework upgraded React Router from 7.12 to 7.18 to pick up security fixes — most notably an arbitrary-code-execution advisory and several denial-of-service advisories in React Router's vendored
turbo-stream/ single-fetch handling. Staying on the older line would leave those unpatched, so the upgrade was the right call; this benign dev warning is a minor side effect of it.Workaround
None needed. Safe to ignore the warning in dev. It disappears entirely in production builds (
pnpm build+ preview/deploy).