fix(solid-router): prevent HeadContent hydration warnings #7510
Conversation
…ion FOUC Fix two related issues with HeadContent in solid-router: 1. Hydration: render bare element templates with one getNextElement per invocation (switch instead of if/return), call hydration-affecting hooks unconditionally, and capture the relocated node via the JSX return value instead of a ref attribute so SSR attributes on void meta/link elements are preserved. Eliminates "unclaimed server-rendered node" warnings. 2. FOUC: stabilize head tag object identity per-key across renders so <For> (which keys by reference) reconciles unchanged tags in place instead of remounting every head node on navigation. Previously a single changed tag (e.g. title) invalidated the whole array, remounting the app stylesheet link and briefly detaching it, causing a flash of unstyled content in dark mode.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
View your CI Pipeline Execution ↗ for commit bef6a9d
☁️ Nx Cloud last updated this comment at |
🚀 Changeset Version Preview6 package(s) bumped directly, 0 bumped as dependents. 🟩 Patch bumps
|
Bundle Size Benchmarks
Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better. |
There was a problem hiding this comment.
Important
At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.
Nx Cloud is proposing a fix for your failed CI:
We introduced a nonceAttr helper in useTags() that only includes the nonce key in element attrs when the nonce is defined (server-side), preventing { nonce: undefined } from being spread onto hydrated elements. Previously, Solid would treat nonce={undefined} during hydration as an instruction to remove the attribute, stripping the server-rendered nonce from every <style>, <link>, <script>, and <meta> element and causing all CSP tests to fail. This change preserves the server-rendered nonce through hydration and restores passing CSP compliance.
Warning
❌ We could not verify this fix.
Suggested Fix changes
diff --git a/packages/solid-router/src/headContentUtils.tsx b/packages/solid-router/src/headContentUtils.tsx
index beddfe3d..b5f3f06a 100644
--- a/packages/solid-router/src/headContentUtils.tsx
+++ b/packages/solid-router/src/headContentUtils.tsx
@@ -17,6 +17,10 @@ import type {
export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => {
const router = useRouter()
const nonce = router.options.ssr?.nonce
+ // Only include nonce in element attrs when it is defined. Spreading
+ // `{ nonce: undefined }` onto a hydrated element causes Solid to *remove*
+ // the nonce attribute that was rendered by the server, which breaks CSP.
+ const nonceAttr = nonce != null ? { nonce } : {}
const getTagKey = (tag: RouterManagedTag) => JSON.stringify(tag)
const activeMatches = Solid.createMemo(
() => router.stores.activeMatchesSnapshot.state,
@@ -74,7 +78,7 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => {
tag: 'meta',
attrs: {
...m,
- nonce,
+ ...nonceAttr,
},
})
}
@@ -109,7 +113,7 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => {
tag: 'link',
attrs: {
...link,
- nonce,
+ ...nonceAttr,
},
})) satisfies Array<RouterManagedTag>
@@ -129,7 +133,7 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => {
crossOrigin:
getAssetCrossOrigin(assetCrossOrigin, 'stylesheet') ??
asset.attrs?.crossOrigin,
- nonce,
+ ...nonceAttr,
},
}) satisfies RouterManagedTag,
)
@@ -156,7 +160,7 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => {
crossOrigin:
getAssetCrossOrigin(assetCrossOrigin, 'modulepreload') ??
preloadLink.crossOrigin,
- nonce,
+ ...nonceAttr,
},
})
}),
@@ -175,7 +179,7 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => {
tag: 'style',
attrs: {
...style,
- nonce,
+ ...nonceAttr,
},
children,
})),
@@ -191,7 +195,7 @@ export const useTags = (assetCrossOrigin?: AssetCrossOriginConfig) => {
tag: 'script',
attrs: {
...script,
- nonce,
+ ...nonceAttr,
},
children,
})),
Or Apply changes locally with:
npx nx-cloud apply-locally mXtR-Jg7l
Apply fix locally with your editor ↗ View interactive diff ↗
🎓 Learn more about Self-Healing CI on nx.dev
Merging this PR will improve performance by 12.25%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ⚡ | client-side navigation loop (solid) |
72.6 ms | 64.7 ms | +12.25% |
Tip
Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.
Comparing fix/solid-router-headcontent-hydration-fouc (7207ebd) with solid-router-v2-pre (67a9040)1
Footnotes
| // When <HeadContent /> is placed in <head> (the SSR/hydration case), the node | ||
| // is already in document.head, so this is a no-op and the element is left | ||
| // exactly where Solid hydrated it. | ||
| function useRelocateToHead(getEl: () => Node | undefined) { |
There was a problem hiding this comment.
We could consider being strict here like react and just requiring the HeadContent to be in head
Fix two related issues with HeadContent in solid-router:
Hydration: render bare element templates with one getNextElement per invocation (switch instead of if/return), call hydration-affecting hooks unconditionally, and capture the relocated node via the JSX return value instead of a ref attribute so SSR attributes on void meta/link elements are preserved. Eliminates "unclaimed server-rendered node" warnings.
FOUC: stabilize head tag object identity per-key across renders so (which keys by reference) reconciles unchanged tags in place instead of remounting every head node on navigation. Previously a single changed tag (e.g. title) invalidated the whole array, remounting the app stylesheet link and briefly detaching it, causing a flash of unstyled content in dark mode.