Skip to content

useHead does not resolve reactive input on the client #1004

Description

@Carpediem94

🐛 The bug

The module's useHead replacement (src/runtime/composables/head.ts) never resolves reactive input. All of its activeHead.push() / patch() sites hand the raw object to unhead, while @unhead/vue's own clientUseHead runs walkResolver(input, VueResolver) inside a watchEffect.

This only bites on the client: @unhead/vue/server creates the head with propResolvers: [VueResolver], so refs resolve inside unhead during SSR. The client head has no prop resolver, so resolution exists only in the composable this module replaces.

Two consequences:

  1. Crash — a ref on innerHTML/textContent of a style/script reaches unhead's normalizeProps, which JSON-stringifies object values:

    TypeError: Converting circular structure to JSON
        --> starting at object with constructor 'ComputedRefImpl'
        |     property 'dep' -> object with constructor 'Dep'
        --- property 'computed' closes the circle
        at JSON.stringify (<anonymous>)
        at normalizeProps (unhead.js:148:28)
        at normalizeTag (unhead.js:175:15)
        at normalizeEntryToTags (unhead.js:209:56)
        at resolveTags (unhead.js:362:16)
        at _renderDOMHead (unhead.js:183:21)
    

    It is thrown inside the debounced _renderDOMHead, so the whole head render is abandoned: the tag never lands in the DOM, and it can abort router start.

  2. Silent, for every other prop: the ref object is used instead of its .value, and since there is no watchEffect, the tag never updates when the ref changes. Client-side reactive head does not work, though unhead v3 documents it as supported.

@nuxt/ui triggers case 1 out of the box: its colors plugin calls useHead({ style: [{ innerHTML: root, id: 'nuxt-ui-colors' }] }) where root is a computed. Any @nuxtjs/ionic + @nuxt/ui app loses its theme colors style. @nuxtjs/i18n's useLocaleHead() and @nuxtjs/seo hit case 2.

🛠️ To reproduce

https://github.com/Carpediem94/nuxt-ionic-head-repro

🌈 Expected behaviour

Reactive input resolved before being pushed to unhead, and kept in sync as it changes, matching the behaviour of @unhead/vue's useHead.

ℹ️ Additional context

@nuxtjs/ionic 1.0.2, nuxt 4.5.2, @unhead/vue 3.3.1, @nuxt/ui 4.10.0.

Same error class as unjs/unhead#869, but that fix only covered the SSR streaming serializer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions