fix: scope per-instance customize to unique CSS selector#483
fix: scope per-instance customize to unique CSS selector#483mukundshah wants to merge 1 commit intonuxt:mainfrom
Conversation
📝 WalkthroughWalkthroughThis change adds per-instance CSS selector hashing to the CSS mode component when a customize function is provided. When Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/runtime/components/css.ts (1)
149-169:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
watchonly observesprops.name, leavingprops.customizechanges un-servicedAfter this PR,
cssClass.value(and thereforeselector.value) depends on bothprops.nameandprops.customize. Thewatchsource is still() => props.name, so if thecustomizeprop is updated whilenamestays the same:
cssClass.valuegets a new--customized-<hash>suffix.- The rendered
<span>immediately receives the new class (reactive binding at line 214).- The watch callback never fires — no
mountCSScall is made for the new selector.- The icon renders with the new class but without a matching CSS rule, so the customization is silently dropped.
🐛 Proposed fix — watch the computed
cssClassinstead of the raw propwatch( - () => props.name, + cssClass, () => { if (selectors.has(selector.value)) { return } const data = getIcon(props.name)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/runtime/components/css.ts` around lines 149 - 169, The watch currently observes only props.name so updates to props.customize (which change cssClass.value and selector.value) won’t trigger mounting CSS; update the watch source to observe the computed cssClass (or include props.customize) instead of just () => props.name so that when cssClass/selector changes the callback runs and calls mountCSS/loadIcon for the new selector; adjust the watch invocation that references watch(() => props.name, ...) to reference the computed cssClass (or a function that returns `${props.name}-${props.customize}`) and keep the existing mountCSS/loadIcon logic intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/runtime/components/css.ts`:
- Around line 85-94: The computed cssClass currently hashes
props.customize.toString(), which can differ between SSR and client builds and
cause hydration mismatches; add a new prop (e.g., customizeKey) that users can
set to a stable string and change the cssClass computation (the computed named
cssClass) to use props.customizeKey as the hash source when present, otherwise
fall back to props.customize.toString(); ensure the hash call (hash(...)) and
the selector prefix (options.cssSelectorPrefix + props.name) remain unchanged
and document the new customizeKey prop so consumers know to provide a stable key
for SSR.
---
Outside diff comments:
In `@src/runtime/components/css.ts`:
- Around line 149-169: The watch currently observes only props.name so updates
to props.customize (which change cssClass.value and selector.value) won’t
trigger mounting CSS; update the watch source to observe the computed cssClass
(or include props.customize) instead of just () => props.name so that when
cssClass/selector changes the callback runs and calls mountCSS/loadIcon for the
new selector; adjust the watch invocation that references watch(() =>
props.name, ...) to reference the computed cssClass (or a function that returns
`${props.name}-${props.customize}`) and keep the existing mountCSS/loadIcon
logic intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: de24b735-fb17-454a-bb25-6b47e5b171b0
📒 Files selected for processing (1)
src/runtime/components/css.ts
| const cssClass = computed(() => { | ||
| if (!props.name) return '' | ||
| const base = options.cssSelectorPrefix + props.name | ||
| // When a per-instance customize function is provided, append a hash of its source | ||
| // so that distinct customizations get their own selector and CSS rule. | ||
| if (typeof props.customize === 'function') { | ||
| return base + '--customized-' + hash(props.customize.toString()) | ||
| } | ||
| return base | ||
| }) |
There was a problem hiding this comment.
fn.toString() may differ between SSR and client bundles, causing hydration mismatches in production
In a typical Nuxt production build, the client bundle is minified/mangled while the SSR bundle is not (or is processed with different transforms). A user-supplied customize arrow function such as:
(attr) => { attr.fill = 'currentColor'; return attr }could serialize differently in each environment:
- SSR:
attr => { attr.fill = 'currentColor'; return attr; } - Client (minified):
a=>(a.fill="currentColor",a)
These produce distinct hashes, so the SSR-rendered <span> class (e.g. …--customized-abc) and the class the client computes on hydration (e.g. …--customized-xyz) will differ. Vue will emit a hydration mismatch warning, and the icon may be unstyled during the initial client paint until mountCSS runs for the client-side hash.
A pragmatic mitigation would be to expose a customize-key prop that users can set to a stable string, and use that as the hash source when present, falling back to fn.toString() only for client-only rendering:
// user-visible stable key avoids serialize-then-hash fragility
if (typeof props.customize === 'function') {
const source = props.customizeKey ?? props.customize.toString()
return base + '--customized-' + hash(source)
}At minimum, the limitation (hash instability in production SSR builds) should be documented.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/runtime/components/css.ts` around lines 85 - 94, The computed cssClass
currently hashes props.customize.toString(), which can differ between SSR and
client builds and cause hydration mismatches; add a new prop (e.g.,
customizeKey) that users can set to a stable string and change the cssClass
computation (the computed named cssClass) to use props.customizeKey as the hash
source when present, otherwise fall back to props.customize.toString(); ensure
the hash call (hash(...)) and the selector prefix (options.cssSelectorPrefix +
props.name) remain unchanged and document the new customizeKey prop so consumers
know to provide a stable key for SSR.
|
I think coderabbit comment is good here, also do you have an example of using cutomize with css that works? |
|
Hi @atinux Here's the repro: https://stackblitz.com/edit/github-fheyzs74 The fix is installed via the patch file. You can revert the patch to reproduce the issue if needed. I’ve also added a screenshot for reference. Regarding the case pointed out by CodeRabbit, we haven’t observed any hydration issues so far. This fix is already running in production without any problems. |
🔗 Linked issue
Resolves #482
📚 Description
In CSS mode, the per-instance
customizeprop was not scoped to its instance; depending on render order, either all instances of the same icon got customized or none did.When
customizeis a function, a hash offn.toString()is now appended to the CSS class name so each distinct customization gets its own selector and CSS rule. The SSR dedup key is updated to match.