Skip to content

Conversation

@KazariEX
Copy link
Member

@KazariEX KazariEX commented Oct 12, 2025

🔗 Linked issue

fix #33268

📚 Description

When rendering a client component, if the placeholder node and the actual root node are elements that has the same tag name, vue runtime will not run the setScopeId again, while the scopeId is also not checked during hydration.

<!-- ssr output -->
<div></div>

<!-- hydration (matched) -->
<div></div>

<!-- mounted (`patchElement` instead of `mountElement`) -->
<div class="foo">...</div>

I'm not sure whether there is any reason that the placeholder is required to be an element.

@KazariEX KazariEX requested a review from danielroe as a code owner October 12, 2025 16:30
@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Oct 12, 2025

Open in StackBlitz

@nuxt/kit

npm i https://pkg.pr.new/@nuxt/kit@33466

nuxt

npm i https://pkg.pr.new/nuxt@33466

@nuxt/rspack-builder

npm i https://pkg.pr.new/@nuxt/rspack-builder@33466

@nuxt/schema

npm i https://pkg.pr.new/@nuxt/schema@33466

@nuxt/vite-builder

npm i https://pkg.pr.new/@nuxt/vite-builder@33466

@nuxt/webpack-builder

npm i https://pkg.pr.new/@nuxt/webpack-builder@33466

commit: 3fd7a59

@coderabbitai
Copy link

coderabbitai bot commented Oct 12, 2025

Walkthrough

Replaces visible placeholder DOM nodes (div) with Vue comment VNodes across multiple Nuxt runtime components. server-placeholder.ts now returns createCommentVNode('placeholder') instead of a div. client-component.ts and client-only.ts replace various fallback and template render paths that previously used h('div') or static div VNodes with createCommentVNode('placeholder') and add the import where required. utils.ts removes the exported utility elToStaticVNode and related static VNode generation code; other utility fragment helpers remain unchanged. No function signatures were altered except the removal of elToStaticVNode.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly describes the primary change of replacing div placeholders with comment nodes before client components mount and aligns with the intent of the pull request. It specifies the context of Nuxt client component rendering and directly reflects the core modifications made across the codebase. It is concise and free of generic language, enabling reviewers to understand the main change at a glance.
Linked Issues Check ✅ Passed The pull request implements the solution for issue #33268 by replacing all div-based placeholders with comment VNodes so that the Vue runtime will reapply scopeId on mounting. The changes span server, client, and utility modules to fully address the missing scoped styles reported in the linked issue. There are no unmet coding objectives from the issue.
Out of Scope Changes Check ✅ Passed All modifications are confined to placeholder rendering logic and the removal of the associated utility, directly supporting the scoped style fix without introducing any unrelated features or code. Each change is tied to client component mounting and hydration behaviour, and no out-of-scope alterations are present.
Description Check ✅ Passed The description directly addresses the code changes by explaining the hydration issue with matching placeholder and root elements and justifies using comment nodes to ensure scopeId application. It references the linked issue and provides examples of SSR and mount behaviour that align with the implementation. Although it includes an author query about placeholder requirements, the description remains on-topic and informative.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca07d4e and 3fd7a59.

📒 Files selected for processing (2)
  • packages/nuxt/src/app/components/client-only.ts (4 hunks)
  • packages/nuxt/src/app/components/utils.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/nuxt/src/app/components/utils.ts
  • packages/nuxt/src/app/components/client-only.ts
🔇 Additional comments (4)
packages/nuxt/src/app/components/client-only.ts (3)

1-1: LGTM: Import addition is correct.

The addition of createCommentVNode to the Vue imports is necessary for the placeholder replacement strategy throughout this file.


83-83: LGTM: Template placeholder matches runtime approach.

The template string correctly uses <!--placeholder--> as the comment node equivalent, which is consistent with the createCommentVNode('placeholder') calls in the runtime render functions.


77-77: Verify comment placeholder handling during client-only hydration
Verify that createCommentVNode('placeholder') at lines 77, 126 and 140 is recognised as a hydration placeholder and replaced without causing DOM insertBefore errors.

packages/nuxt/src/app/components/utils.ts (1)

1-2: Safe to remove elToStaticVNode
No imports or references to elToStaticVNode found; removal in utils.ts is non-breaking.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf102e8 and ca07d4e.

📒 Files selected for processing (2)
  • packages/nuxt/src/app/components/server-placeholder.ts (1 hunks)
  • packages/nuxt/src/components/runtime/client-component.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/nuxt/src/app/components/server-placeholder.ts
  • packages/nuxt/src/components/runtime/client-component.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: code
🔇 Additional comments (6)
packages/nuxt/src/app/components/server-placeholder.ts (1)

1-1: LGTM: Import addition is correct.

The import of createCommentVNode is appropriate for the new implementation.

packages/nuxt/src/components/runtime/client-component.ts (5)

1-1: LGTM: Import addition is correct.

Adding createCommentVNode to the imports from 'vue' is necessary for the new placeholder implementation.


35-37: LGTM: Non-script-setup render override correctly uses comment node.

The fallback for the non-script-setup render override now uses a comment node placeholder, which prevents Vue's runtime from confusing it with the actual component root when they share the same tag name.


40-43: LGTM: Template syntax correctly uses HTML comment for placeholder.

The runtime-compiler template fallback correctly uses the HTML comment syntax <!--placeholder-->, which will be compiled by Vue's template compiler into the equivalent of createCommentVNode('placeholder').


59-66: LGTM: Async setup fallback correctly uses comment node.

The fallback for asynchronous setup functions now returns a comment node placeholder, maintaining consistency with the other changes in this file.


68-72: LGTM: Synchronous setup fallback correctly uses comment node.

The fallback for synchronous setup functions that return render functions now uses a comment node placeholder, completing the consistent replacement of all placeholder divs in this file.

@codspeed-hq
Copy link

codspeed-hq bot commented Oct 12, 2025

CodSpeed Performance Report

Merging #33466 will not alter performance

Comparing KazariEX:fix/issue-33268 (3fd7a59) with main (3c38d1f)

Summary

✅ 10 untouched

@danielroe
Copy link
Member

it looks like we are now getting:

Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.

@KazariEX
Copy link
Member Author

Should we change the expected behavior of this test entry?

nuxt/test/basic.test.ts

Lines 142 to 143 in 3c38d1f

// should apply attributes to client-only components
expect(html).toContain('<div style="color:red;" class="client-only"></div>')

@danielroe
Copy link
Member

maybe we can specify the fallback element (and also verify that the attribute is applied correctly when hydrated?)

@KazariEX
Copy link
Member Author

It seems like there are a lot of test cases that need to be adjusted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Why doesn't the class style take effect when only the client renders the page in nuxt4.1.2 version?

2 participants