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

Allow users customize ID generation #2959

Merged
merged 4 commits into from Feb 2, 2024
Merged

Allow users customize ID generation #2959

merged 4 commits into from Feb 2, 2024

Conversation

thecrypticace
Copy link
Contributor

@thecrypticace thecrypticace commented Feb 1, 2024

Resolves #2913

We implement our own ID generation in @headlessui/vue because Vue has not historically offered a native useId helper. However, our implementation uses global state which means that long-running servers do not use fresh IDs for every request. Because of this users will encounter hydration mismatches on subsequent requests. These mismatches weren't warned about until Vue v3.4. Because of this, in the future, Vue v3.5 will be providing a native useId helper that lets us address this issue. In the meantime, Nuxt v3.10+ has implemented a custom useId helper that is guaranteed to work across the server/client boundary to eliminate these hydration mismatches.

This PR allows users to override our internal ID generation which can then use Nuxt's useId composable to ensure that IDs are always fresh, unique, and consistent across the server/client boundary and don't result in hydration mismatches.

Warning

This API is a temporary measure and not guaranteed to work long-term. Once Vue v3.5 is out we'll switch to using the offical method for generating IDs.

This new API in Headless UI can be used by calling provideUseId(…) with a custom function. For Nuxt users, the function you pass should call the useId composable that it provides (no need to import, it's automatic) like so:

<template>
  <div>
    <Switch v-model="enabled">
      Thing
    </Switch>
  </div>
</template>

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

let enabled = ref(false)

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

Vue does not currently have a native `useId` helper. However, Nuxt has created their own that they ensure works across the client/server boundary.

Now a user can use `provide()` in their app to inject a custom useId generation function which, for Nuxt users, can defer to the one provided by Nuxt.
Copy link

vercel bot commented Feb 1, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
headlessui-react ❌ Failed (Inspect) Feb 2, 2024 2:39pm
headlessui-vue ❌ Failed (Inspect) Feb 2, 2024 2:39pm

@adamwathan
Copy link
Member

adamwathan commented Feb 2, 2024

What do you think about exposing this as a custom hook instead of a raw provide call with a string key? We can use a Symbol internally for the key which feels a bit cleaner and avoids any risk of naming collisions, and then the API will feel a bit cleaner to end users as well.

Rough example (would need to think through name):

<template>
  <div>
    <Switch v-model="enabled">
      Thing
    </Switch>
  </div>
</template>

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

provideUseId(() => useId())

let enabled = ref(false)
</script>

Copy link
Collaborator

@RobinMalfait RobinMalfait left a comment

Choose a reason for hiding this comment

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

I like @adamwathan's suggestion so that we don't have to expose a "magic" string.

This is also implemented against the 1.x branch. Should we also open a PR to do this against latest main, or is the idea to wait for the useId hook from Vue itself?

@thecrypticace
Copy link
Contributor Author

My idea was intended to not introduce an API that, if removed, would break builds once we require Vue v3.5. But we could just as easily keep it around and it do nothing once we've done that.

As for Headless UI v2, either option seems fine to me.

@thecrypticace
Copy link
Contributor Author

Updated implementation to use provideUseId instead.

@reinink
Copy link
Member

reinink commented Feb 2, 2024

Thinking about the name here and wondering if there's something better than provideUseId? I know that technically this feature allows someone to provide their own "use id" callback, but I don't feel like I've seen precedence for composables that start with "provide" — it's normally "use" or "define"

For example, what do you think of defineUseId, useCustomId or even useIdProvider? Personally kind of like defineUseId.

@adamwathan
Copy link
Member

Is there any prior art we can reference here for how other popular Vue libraries name custom hooks that do similar things? Maybe @AlexVipond has an idea since he's pretty tuned in to the Vue composition API ecosystem.

Another idea barring some established convention we can follow is something like generateIdsUsing(...).

@AlexVipond
Copy link

TL;DR provide<Something> is the best name IMO

It's an uncommon enough pattern that there's no widely accepted naming convention, but I went with provide<Something> when I used this same pattern to enable app-wide event delegation for tracking pressed/active state.

That's partly because of my own personal naming convention that reserves use<Something> for functions that use reactivity APIs and/or component lifecycle hooks, AND have a return value that includes reactive state. provide<Something>, has no return value, so I don't name it with use.

The Vue docs on this topic recommend use for composables, but they also define composables as functions that encapsulate and reuse stateful logic. provide<Something> isn't really stateful logic, it's just performing an app-wide side effect.

@AlexVipond
Copy link

I'd steer away from define because it's usually reserved for no-op type definition helpers

@adamwathan
Copy link
Member

@AlexVipond Awesome thanks so much man, appreciate the sanity check on this one!

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.

None yet

5 participants