feat(nuxt): `useId` composable
๐ Linked issue
โ Type of change
- [ ] ๐ Documentation (updates to the documentation, readme or JSdoc annotations)
- [ ] ๐ Bug fix (a non-breaking change that fixes an issue)
- [ ] ๐ Enhancement (improving an existing functionality like performance)
- [x] โจ New feature (a non-breaking change that adds functionality)
- [ ] ๐งน Chore (updates to the build process or auxiliary tools and libraries)
- [ ] โ ๏ธ Breaking change (fix or feature that would cause existing functionality to change)
๐ Description
The useId composable creates a unique id.
๐ Checklist
- [ ] I have linked an issue or discussion.
- [x] I have updated the documentation accordingly.
Run & review this pull request in StackBlitz Codeflow.
Live Preview ready!
| Name | Edit | Preview | Latest Commit |
|---|---|---|---|
| Nuxt Docs | Edit on Studio โ๏ธ | View Live Preview | c8e26c9becf6012b4a3c184cea3439150bc0fdf8 |
This is a nice idea however I think there are easy pitfalls cases if we base such broadly named composable on transform-based key generators for runtime extending limitations currently we have with key-based data fetching composables.
- Using
getUniqueIDin a component that is rendered multiple times (like a UI text input which i suppose would be common for accessibility), it always uses one ID which is not probably what we want
-
Using
getUniqueID()in reusable utilities (even in same SFC!) or externalized dependencies won't be effective and can lead to runtime issues (we could have runtime fallback and hydrate it somehow in that case) -
getUniqueID()composable naming sounds it generates an increamental/unique name by each call which it won't -
The
getUniqueIDcan be possibly conflict with future nitro/uncrypto auto imported utils sharing common name (maybe also otherlibs or even vue-core sometime!)
Having these in mind, i think might makes sense to make sure we use a name that is clear it comes from nuxt and also it is an code-location-based static key. Something like getNuxtStaticID() (name is terrible just to give some idea)
Another idea might be to make the usage to be like a constant, instead of function calling like:
const id = '' /* nuxt-unique-id */
You can try adding a local ID
const nuxt = useNuxtApp()
const localId = (nuxt.payload?.localIds?.[key] ?? 0) + 1
nuxt.payload.localIds = nuxt.payload.localIds ?? {}
nuxt.payload.localIds[key] = localId
Then repeatedly calling the component will not cause conficts
<script setup lang="ts">
import {getUniqueID} from '#imports'
withDefaults(defineProps<{ id?: string }>(), {
id: () => getUniqueID()
});
</script>
<template>
<label :for="id">Label</label>
<br>
<input type="text" :id="id" :placeholder="id"/>
</template>
Given that the main use case for this utility is to generate unique IDREFs (for labeling inputs and creating other ARIA associations), I would expect that it is:
- unique per component instance
- stable after hydration
Here's a blog post that has some useful ideas and links: https://www.jovidecroock.com/blog/preact-use-id
Those are both good suggestions. A hybrid approach that gets a unique constant and uses it as prefix + an additional per-instance value seems good to me.
Marking as draft for now - do feel free to change that whenever it's good to go ๐
I would expect that it is:
- unique per component instance
I would say it should be unique not only per component instance, but globally unique (per HTML page). Because different instances of the same component on a single page must not have collisisons of IDs of their inner HTML elements.
That is why I think the prefix should be optional. We need global uniqueness anyway.
Maybe, if no prefix is given, the composable should use the name of the current component as a prefix. This will not add any functionality (as the generated ID will still be unique even without such prefix), but will make generated ID more debug-friendly. Of course, this solution will force this helper function to be composable instead of just a helper function.
Example (Username.vue):
<script setup>
const componentId = useUniqueComponentId()
</script>
<template>
<div>
<label :for="`${componentId}:username`">Username</label>
<input :id="`${componentId}:username`">
</div>
</template>
I don't like the string interpolation inside the template, to be honest. But I have no idea how to improve it. Of course, we could just generate unique ID for each element inside component (by calling useUniqueComponentId() multiple times), but then there will be no semantic at all in these IDs. All IDs will be just random strings. I mean, having ID like f3f3eg9-username or ${componentName}-f3f3eg9-username is better than just f3f3eg9 or ${componentName}-f3f3eg9.
Also, the scenario for having label and input inside of v-for should be covered. We can't pre-generate unique IDs for every element inside of v-for. So, it is useful to pre-generate a unique "prefix" of the element ID (such as componentId in the example above) and have dynamic "suffix" (e.g. v-for key).
Another idea is to have something like <style scoped> but for <template>. I.e.:
<template scoped>
<div>
<label :for="username">Username</label>
<input :id="username">
</div>
</template>
will generate:
<div>
<label :for="Username-f3f3eg9-username">Username</label>
<input :id="Username-f3f3eg9-username">
</div>
So, the developer don't need to think about ID uniqueness. But, I'm not sure it could be implemented in a generic way (to support any possible HTML elements) and, of course, such feature would be more suitable for Vue itself, than Nuxt.
I would say it should be unique not only per component instance, but globally unique (per HTML page). Because different instances of the same component on a single page must not have collisisons of IDs of their inner HTML elements.
yes, i should have clarified: what i really mean is it should be unique for each component instance, regardless of whether that is two instances of the same component or one instance each of two different components.
<Comp1 /> // id1
<Comp1 /> // id2
<Comp2 /> // id3
I'm marking this as draft because I think it needs to produce globally unique (and stable) ids tied to component instances, as the last two comments have pointed out.
Added back to useId to be similar to React useId to avoid more mental work and since it has to be used inside a component.
I updated it to generate unique ID per component instance as well as being able to generate multiple unique id in the same component instance.
CodSpeed Performance Report
Merging #23368 will degrade performances by 36.95%
:warning: No base runs were found
Falling back to comparing TakNePoidet:main (049d48f) with main (1a9fb57)
Summary
โก 1 improvements
โ 1 regressions
โ
6 untouched benchmarks
:warning: Please fix the performance issues or acknowledge them on CodSpeed.
Benchmarks breakdown
| Benchmark | main |
TakNePoidet:main |
Change | |
|---|---|---|---|---|
| โ | minimal test fixture |
39.2 ms | 62.2 ms | -36.95% |
| โก | minimal test fixture (types) |
136.3 ms | 42.4 ms | ร3.2 |
/trigger release
:rocket: Release triggered! You can now install nuxt@npm:nuxt-nightly@pr-23368
Linking https://github.com/vuejs/rfcs/discussions/557 here
/trigger release
:rocket: Release triggered! You can now install nuxt@npm:nuxt-nightly@pr-23368
Given that the main use case for this utility is to generate unique
IDREFs (for labeling inputs and creating other ARIA associations), I would expect that it is:
- unique per component instance
- stable after hydration
Here's a blog post that has some useful ideas and links: https://www.jovidecroock.com/blog/preact-use-id
I have given it a try locally (while also fixing some edge cases that are present in its current state), but I was unable to find a way to make Preact's solution work in SSR.
On the client, it is possible to locate a certain vnode's path by traversing up the tree until you reach the root, storing each node's index on its parent's children. But on the server, the instance.subTree property is null and after debugging I didn't find any information about the current vnode that could either help specify its path, or at least be used as a unique identifier that remains stable after hydration (instance.uid is not stable for example).
If we are able to locate the vnode's path in the tree on the server, or have some unique identifier to each instance that remains stable after hydration, then this composable could be used in any component without having to store the ids in the root element's attributes (it could go directly to the payload as a Record<string, number[]>), which means that its template would be completely irrelevant to whether you can use useId().
I'm not very well-versed in either Nuxt or Vue internals, so please tell me if I've missed something.
As a note - this is planned for Vue 3.5 to be supported natively. Source
Oh that is very good news, thanks for sharing. Should be much more reliable this way as Vue handles the vnode tree.
Exactly, I believe it should come from Vue core
Does that mean that this isn't coming to Nuxt and will be entirely implemented in Vue core?
I get the following error when trying to use useId():
The requested module '*/node_modules/nuxt/dist/app/components/client-only.js?v=d16211f9' does not provide an export named 'clientOnlySymbol'
Try removing node_modules/.cache. It sounds like vite may not have updated its cache with the new version.
@danielroe fixed it! Thanks a lot.
are you sure this is ssr friendly ?
@DJafari i am actually having the same problem.
If you have an issue, please open a new issue with a reproduction rather than commenting on this PR ๐
@danielroe Because I cannot recreate this problem in the new project, I am commenting here
but it's has a bug, in some case data-n-ids attribute doesnt add to element
i use useId in two component, in one component data-n-ids added, but in another, not
@DJafari no matter if you can reproduce it or not, commenting on a merged PR is not the place to report bugs. That should be an own issue. If you want reproduce the issue the problem might be part of your code then?