nuxt icon indicating copy to clipboard operation
nuxt copied to clipboard

feat(nuxt): `useId` composable

Open TakNePoidet opened this issue 2 years ago โ€ข 11 comments

๐Ÿ”— 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.

TakNePoidet avatar Sep 22 '23 16:09 TakNePoidet

Review PR in StackBlitz Codeflow 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

nuxt-studio[bot] avatar Sep 22 '23 16:09 nuxt-studio[bot]

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 getUniqueID in 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
image
  • 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 getUniqueID can 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 */

pi0 avatar Sep 28 '23 09:09 pi0

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>

image

TakNePoidet avatar Sep 28 '23 11:09 TakNePoidet

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

mayank99 avatar Sep 28 '23 18:09 mayank99

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 ๐Ÿ™

danielroe avatar Sep 29 '23 10:09 danielroe

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.

xak2000 avatar Dec 11 '23 15:12 xak2000

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

mayank99 avatar Dec 11 '23 16:12 mayank99

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.

danielroe avatar Dec 14 '23 12:12 danielroe

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.

atinux avatar Dec 20 '23 12:12 atinux

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

codspeed-hq[bot] avatar Dec 25 '23 13:12 codspeed-hq[bot]

/trigger release

atinux avatar Jan 02 '24 10:01 atinux

:rocket: Release triggered! You can now install nuxt@npm:nuxt-nightly@pr-23368

github-actions[bot] avatar Jan 02 '24 11:01 github-actions[bot]

Linking https://github.com/vuejs/rfcs/discussions/557 here

TheAlexLichter avatar Jan 06 '24 22:01 TheAlexLichter

/trigger release

atinux avatar Jan 08 '24 17:01 atinux

:rocket: Release triggered! You can now install nuxt@npm:nuxt-nightly@pr-23368

github-actions[bot] avatar Jan 08 '24 17:01 github-actions[bot]

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.

alex-eliot avatar Jan 11 '24 10:01 alex-eliot

As a note - this is planned for Vue 3.5 to be supported natively. Source

TheAlexLichter avatar Jan 11 '24 10:01 TheAlexLichter

Oh that is very good news, thanks for sharing. Should be much more reliable this way as Vue handles the vnode tree.

alex-eliot avatar Jan 11 '24 11:01 alex-eliot

Exactly, I believe it should come from Vue core

atinux avatar Jan 11 '24 15:01 atinux

Does that mean that this isn't coming to Nuxt and will be entirely implemented in Vue core?

cernymatej avatar Jan 11 '24 15:01 cernymatej

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'

fabkho avatar Feb 01 '24 10:02 fabkho

Try removing node_modules/.cache. It sounds like vite may not have updated its cache with the new version.

danielroe avatar Feb 01 '24 18:02 danielroe

@danielroe fixed it! Thanks a lot.

fabkho avatar Feb 01 '24 18:02 fabkho

image are you sure this is ssr friendly ?

DJafari avatar Feb 02 '24 18:02 DJafari

@DJafari i am actually having the same problem.

fabkho avatar Feb 03 '24 09:02 fabkho

If you have an issue, please open a new issue with a reproduction rather than commenting on this PR ๐Ÿ™

danielroe avatar Feb 03 '24 10:02 danielroe

@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

image

DJafari avatar Feb 03 '24 11:02 DJafari

@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?

TheAlexLichter avatar Feb 03 '24 11:02 TheAlexLichter