Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Disclosure] Nuxt SSR Hydration missmatch #2913

Closed
mwohlan opened this issue Jan 4, 2024 · 24 comments
Closed

[Disclosure] Nuxt SSR Hydration missmatch #2913

mwohlan opened this issue Jan 4, 2024 · 24 comments
Assignees

Comments

@mwohlan
Copy link

mwohlan commented Jan 4, 2024

What package within Headless UI are you using?

@headlessui/vue

What version of that package are you using?

insiders but its the same with 1.7.16

What browser are you using?

See Stackblitz

Reproduction URL

https://stackblitz.com/edit/github-ng1nkc?file=app.vue

Describe your issue

The server rendered ids for the button and panel differ from what is expected on the client causing a hydration missmatch warning/error in the console:

Screenshot from 2024-01-04 10-40-56

Could be related to #2624 and #2645

@4350pChris
Copy link

Same problem here. It seems the server does not reset its counter, as it keeps incrementing the id values whenever I refresh the page.

@itpropro
Copy link

itpropro commented Jan 8, 2024

The same thing happens for the menu component:

image

Could this be a general problem and could the fix from #2645 (useId) be generally applied @RobinMalfait ?

@hex-ci
Copy link

hex-ci commented Jan 9, 2024

Same problem here.

@jontybrook
Copy link

Same problem here. It could be unrelated - but I believe this issue started showing up when I upgraded to Nuxt 3.9 (and thus Vue 3.4, vite 5 etc)

@alex-eliot
Copy link

This problem also occurs on the MenuButton, MenuItems, and MenuItem components on Nuxt.

@itpropro
Copy link

itpropro commented Jan 13, 2024

Also occurs on headlessui-listbox-button, basically every button component. Any update @RobinMalfait ?
If the fix would be to use the same useId composable from #2645, I could create a PR for that. But I think it would make sense if you could create a more generic ssr test for all the components using id, so we don't have to duplicate the ssr test code each time :)

@ErriourMe
Copy link

ErriourMe commented Jan 20, 2024

The same problem with Popover component.
Nuxt 3.9.3, Vue 3.4.15, HeadlessUI 1.7.17
image

@brettshepherd
Copy link

Same problem for me as well. Anyone find a temporary fix for this until the library gets patched?

@xak2000
Copy link

xak2000 commented Jan 25, 2024

@brettshepherd I found a workaround. It's not perfect, but at least it works.

I put the problematic parts of the template into <ClientOnly> component. This makes element IDs to be assigned only once (on the client). The problem of this solution is that the part of the component is not rendered until page is fully loaded. That makes the layout blinky as HTML elements could change their width/height after page is loaded.

To workaround that layout problem, I use #fallback feature of <ClientOnly> component, when possible:

  <ClientOnly>
    <Menu as="div" class="..." v-bind="$attrs">
      ...
        <MenuButton class="...">
          <EllipsisVerticalIcon class="h-5 w-5" aria-hidden="true" />
        </MenuButton>
      ...
    </Menu>
    <template #fallback>
      <div class="h-5 w-5" /> <-- the same width/height as the <MenuButton>, that will be rendered only on the Client
    </template>
  </ClientOnly>

I'm not happy with this workaround at all. Looking forward for the fix of this issue. 👍

@mikenavi
Copy link

as a temp workaround you can globally override vue version in your package.json like this:

...
"overrides": {
    "vue": "^3.3.13"
},
...

@manniL
Copy link

manniL commented Jan 29, 2024

I briefly mentioned that in my video about the Vue 3.4 hydration updates. These warnings are "no problem" for now and ideally will be fixed when Vue provides a way to generate random id's which can be easily passed over from server to the client.

Functionality-wise, things should work as before as the "hydration problems" existed before but weren't checked from Vue.

Possibly, a similar approach to https://github.com/danielroe/vue-bind-once could be taken to stabilize the ID generation until Vue (probably 3.5) will provide a native method.

@Foresteam
Copy link

Just encountered this issue, with tab component (HeadlessTab)

- rendered on server: id="headlessui-tabs-tab-18"
- expected on client: id="headlessui-tabs-tab-2"

@ExEr7um
Copy link

ExEr7um commented Jan 31, 2024

Nuxt v3.10 added useId composable for generating SSR-safe unique IDs (nuxt/nuxt#23368).

@itpropro
Copy link

Nuxt v3.10 added useId composable for generating SSR-safe unique IDs (nuxt/nuxt#23368).

Maybe it will also land in vue in time 🤞

@reinink reinink self-assigned this Feb 1, 2024
@reinink
Copy link
Member

reinink commented Feb 1, 2024

Hey folks! 👋

Thanks for reporting this issue and sharing this extra information.

The issue

Here is the issue as I understand it today:

Vue.js improved its hydration issue detection in v3.4, which is why these warnings started appearing. If you downgrade to Vue.js v3.3, you won't see these warnings anymore, although technically the SSR hydration issues still exist.

These improvements in Vue.js v3.4 are highlighting an SSR id generation issue in Headless UI. Right now Headless UI uses a global incrementing id:

let id = 0
function generateId() {
return ++id
}
export function useId() {
return generateId()
}

And while this guarantees that the ids are unique, it doesn't guarantee that they will be the same on both the server and client. And when they don't match, a hydration warning is shown.

Vue.js fix coming in v3.5

Unfortunately there isn't a reliable way today for us to generate an SSR-friendly unique id in Headless UI, as we don't have control over the server.

What we really need is something like React's useId() hook — which is actually exactly what the React version of Headless UI uses today for id generation.

Fortunately, there's some good news! According to this tweet from Evan You, it sounds like Vue.js will be adding a useId() composable in v3.5. Once this is available we'll definitely update Headless UI to use this more SSR-compatible method of id generation.

Temporary fix in Nuxt

As noted elsewhere in this issue, Nuxt (which does have control over the server) has come up with their own useId() composable (nuxt/nuxt#23368) as a stopgap until a more official solution is added to Vue.js in v3.5.

While this new useId() composable has already been released in Nuxt v3.10, Headless UI can't just use it, as not all Headless UI projects are Nuxt projects.

That said, we're currently working on a solution for Nuxt users where you'll be able to "provide" Headless UI with this more reliable useId() composable.

We actually already have a working prototype locally. This is what it will look like to implement in your Nuxt projects:

Note

This doesn't actually exist in Headless UI yet, this is coming soon!

<!-- app.vue -->

<template>
  <Switch>
    <!-- .... -->
  </Switch>
</template>

<script setup>
import { Switch } from '@headlessui/vue'

provide('headlessui.useid', () => useId('myapp-'))
</script>

If all goes well, this temporary fix could be available as early as tomorrow. Just keep in mind that you'll need to be on the latest version of Nuxt (at least v3.10) plus, of course, the latest version of Headless UI when we get that out.

@ExEr7um
Copy link

ExEr7um commented Feb 2, 2024

Hey @reinink.

That's great news! I should also mention, that there is a Nuxt Module for Headless UI (https://github.com/P4sca1/nuxt-headlessui) and I think it can incorporate that temporary fix.

@P4sca1
Copy link

P4sca1 commented Feb 2, 2024

Once the proposed temporary solution @reinink mentioned is available, I will try to incorporate it into the Nuxt HeadlessUI module to make use of it.
However, it might be more complicated, as the useId hook currently can only be called at the component level and not within plugins.
https://github.com/nuxt/nuxt/blob/10f2356ab4b055221d5d63712a98ba6d8cdc25a4/packages/nuxt/src/app/composables/id.ts#L19

@reinink
Copy link
Member

reinink commented Feb 2, 2024

Hey folks! As promised, we've got a fix in place for this as of @headlessui/vue@1.7.18, which was just published a few minutes ago.

We decided to create a small provideUseId() composable that you can import as a way of providing the new Nuxt useId() composable that's available as of Nuxt v3.10. Here's how:

<!-- app.vue -->

<template>
  <Switch>
    <!-- .... -->
  </Switch>
</template>

<script setup>
import { Switch, provideUseId } from '@headlessui/vue'

provideUseId(() => useId())
</script>

As mentioned yesterday, make sure you're on the latest version of both Headless UI and Nuxt:

npm install @headlessui/vue@latest nuxt@latest

We're currently working on Headless UI v2.0, but that will likely come out officially after Vue.js v3.5, so in all likelihood we will just use the native useId() support for the next major release.

Going to close this issue now, but please report back if you have any further issues 👍

@xak2000
Copy link

xak2000 commented Feb 3, 2024

@reinink Can you clarify, please, where to put this provideUseId(() => useId()) line?

Should it be present in every component, that uses any headlessui component? Or it is enough to call it just once at a global level (such as app.vue component in Nuxt)?

@thecrypticace
Copy link
Contributor

@xak2000 It uses provide() and inject() internally so it's sufficient to call it once in the top-most component in your app. e.g. app.vue

@mwohlan
Copy link
Author

mwohlan commented Feb 5, 2024

@reinink I am still getting hydration missmatches even after the fix:

This is what it looks like now your provided solution for nuxt:

image

The question remains if this is an issue with useId or with headlessui ?

@thecrypticace
Copy link
Contributor

@mwohlan can you provide a reproduction?

@mwohlan
Copy link
Author

mwohlan commented Feb 5, 2024

@thecrypticace

https://stackblitz.com/edit/github-bbxtfw?file=app.vue

Okay this seems to be an issue with the nuxt-headlessui module. Importing the the components explicitly from headless-ui fixes the hydration missmatch. This is kinda weird since I forced the module to use the fixed version...

@mikezange
Copy link

mikezange commented May 21, 2024

Appreciate this is not Nuxt but if you ran into this issue (like me) using:
Astro v4.8.6
Vue v3.4.27
HeadlessUI 1.7.22

You can add the client:only="vue" directive to the outer component, it is very much a work around like: #2913 (comment)

But until its fixed permanently it cleans up the console

Example:

Layout.astro
...
<Nav client:only="vue" />
...
Nav.vue
<template>
  <Disclosure as="nav" class="bg-white shadow" v-slot="{ open }">
      ....
  </Disclosure>
</template>

I am by no means a frontend pro, so I don't know if this will have any knock on effects, but everything seems to be working for my nav opening etc.

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

No branches or pull requests