Skip to content

fix: prevent circular reference stack overflow in payload.__hints#289

Merged
huang-julien merged 1 commit intonuxt:mainfrom
martijndewit:fix/payload-circular-refs
Mar 17, 2026
Merged

fix: prevent circular reference stack overflow in payload.__hints#289
huang-julien merged 1 commit intonuxt:mainfrom
martijndewit:fix/payload-circular-refs

Conversation

@martijndewit
Copy link
Contributor

🔗 Linked issue

resolves #284 caused by #240

📚 Description

Problem

PR #240 moved hints data from nuxtApp.__hints to nuxtApp.payload.__hints.

Since payload is reactive, Vue's traverse() recurses infinitely on circular references (component instances, vnodes, DOM elements), causing RangeError: Maximum call stack size exceeded.

This only happens when the application has for example hydration error's and the hints module is displaying them.

Fix

With this fix all deep-traverses should be fixed. The plain [] inside the reactive payload and the ref() to shallowRef()

  • hydration/composables.ts: Wrap instance and vnode with markRaw() before storing in payload
  • web-vitals/plugin.client.ts: Use shallowRef() instead of ref() (metrics contain DOM element references)
  • third-party-scripts/plugin.client.ts: Use shallowRef() instead of ref() (stores HTMLScriptElement references)

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 17, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/hints@289

commit: 5418179

@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

The PR reduces the depth of reactivity tracking for data containing DOM element references stored in nuxtApp.payload. Three files are updated: markRaw() is applied in the hydration composable to prevent Vue from wrapping DOM references, and ref() is replaced with shallowRef() in the web-vitals and third-party-scripts plugins. shallowRef() only tracks top-level changes and avoids deep traversal into nested DOM properties. Runtime behavior and access patterns remain unchanged.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main objective: preventing circular reference stack overflow in payload.__hints through strategic use of markRaw and shallowRef.
Description check ✅ Passed The description clearly explains the problem, the root cause from PR #240, and the three specific fixes applied across the three modified files.
Linked Issues check ✅ Passed The PR implements all three recommended solutions from issue #284: markRaw() wrapping in hydration/composables.ts, shallowRef() in web-vitals/plugin.client.ts, and shallowRef() in third-party-scripts/plugin.client.ts to prevent infinite Vue traversal on circular references.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the circular reference issue in payload.__hints; no unrelated modifications or scope creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

You can validate your CodeRabbit configuration file in your editor.

If your editor has YAML language server, you can enable auto-completion and validation by adding # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json at the top of your CodeRabbit configuration file.

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/third-party-scripts/plugin.client.ts (1)

62-76: ⚠️ Potential issue | 🟠 Major

shallowRef mutations won't trigger reactivity in downstream computed properties.

The scripts ref is exposed directly to consumers via useHostThirdPartyScripts(), which use it in computed properties that depend on scripts.value. At lines 69 and 75 in the plugin, in-place mutations (array.push() and object.loaded = true) don't notify subscribers when using shallowRef, preventing the UI from updating when scripts are added or their loaded state changes.

Call triggerRef(scripts) after each mutation to force reactivity:

Proposed fix
-import { defineNuxtPlugin, shallowRef, useNuxtApp } from '#imports'
+import { defineNuxtPlugin, shallowRef, triggerRef, useNuxtApp } from '#imports'

     nuxtApp.hook('hints:scripts:added', (script: HTMLScriptElement) => {
       scripts.value.push({ element: script, loaded: false })
+      triggerRef(scripts)
     })

     nuxtApp.hook('hints:scripts:loaded', (script: HTMLScriptElement) => {
       const existingScript = scripts.value.find(s => s.element === script)
       if (existingScript) {
         existingScript.loaded = true
+        triggerRef(scripts)
       }
       else {
         logger.warn(...)
         scripts.value.push({ element: script, loaded: true })
+        triggerRef(scripts)
       }
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/third-party-scripts/plugin.client.ts` around lines 62 - 76, The
scripts shallowRef is mutated in-place (via scripts.value.push(...) in the
nuxtApp.hook('hints:scripts:added') handler and existingScript.loaded = true in
the nuxtApp.hook('hints:scripts:loaded') handler) which doesn't notify
consumers; after each mutation call triggerRef(scripts) to force reactivity so
computed consumers (e.g. useHostThirdPartyScripts() users) update — add
triggerRef(scripts) immediately after the push and immediately after setting
existingScript.loaded = true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/runtime/third-party-scripts/plugin.client.ts`:
- Around line 62-76: The scripts shallowRef is mutated in-place (via
scripts.value.push(...) in the nuxtApp.hook('hints:scripts:added') handler and
existingScript.loaded = true in the nuxtApp.hook('hints:scripts:loaded')
handler) which doesn't notify consumers; after each mutation call
triggerRef(scripts) to force reactivity so computed consumers (e.g.
useHostThirdPartyScripts() users) update — add triggerRef(scripts) immediately
after the push and immediately after setting existingScript.loaded = true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 653eaeea-3d5c-4773-bf8e-f3ad15875899

📥 Commits

Reviewing files that changed from the base of the PR and between 5d127f8 and 5418179.

📒 Files selected for processing (3)
  • src/runtime/hydration/composables.ts
  • src/runtime/third-party-scripts/plugin.client.ts
  • src/runtime/web-vitals/plugin.client.ts

@huang-julien huang-julien self-requested a review March 17, 2026 20:58
Copy link
Member

@huang-julien huang-julien left a comment

Choose a reason for hiding this comment

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

Thank you !

@huang-julien huang-julien merged commit 46ce13a into nuxt:main Mar 17, 2026
4 checks passed
@edimitchel
Copy link

Glad to see this issue, I just got the same issue today and I didn't understand what going wrong.

@huang-julien
Copy link
Member

I'll make a patch release tonight, in the meantime, you can use the nightly release

@huang-julien huang-julien mentioned this pull request Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Maximum call stack size exceeded when devtools enabled - payload.__hints stores DOM refs causing infinite traverse

3 participants