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

feat(runtime-core): useId() #11404

Merged
merged 3 commits into from
Jul 19, 2024
Merged

feat(runtime-core): useId() #11404

merged 3 commits into from
Jul 19, 2024

Conversation

yyx990803
Copy link
Member

@yyx990803 yyx990803 commented Jul 19, 2024

Similar to React's useId, this composable returns a unique ID that can be used for form elements and accessibility attributes.

The generated IDs look like v:1-2-3 and are unique across each app instance and are stable across server rendering and client rendering. This is ensured by dividing an app into async boundaries (async components, async setup, serverPrefetch). The order of appearance of direct child async boundaries are always consistent assuming the same data is used between server and client, but their order of resolution may be different. Even if two async boundaries resolve in different orders between server and client, useId() calls inside the two boundaries should not affect each other.

Example Usage

<script setup>
import { useId } from 'vue'

const id = useId()
</script>

<template>
  <form>
    <label :for="id">Name:</label>
    <input :id="id" type="text" />
  </form>
</template>

App Scoped

Note IDs generated are unique-per-app-instance. If you have multiple apps on the same page, you can configure an app-level prefix for each app via app.config.idPrefix:

const app = createSSRApp({ ... })

app.config.idPrefix = 'app1'

Copy link

github-actions bot commented Jul 19, 2024

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 93.9 kB (+3.9 kB) 35.7 kB (+1.24 kB) 32.2 kB (+1.1 kB)
vue.global.prod.js 151 kB (+4.86 kB) 55.5 kB (+1.5 kB) 49.4 kB (+1.35 kB)

Usages

Name Size Gzip Brotli
createApp 53.2 kB (+3.44 kB) 20.6 kB (+1.07 kB) 18.8 kB (+957 B)
createSSRApp 56.7 kB (+3.44 kB) 22 kB (+1.04 kB) 20.1 kB (+976 B)
defineCustomElement 55.5 kB (+3.44 kB) 21.4 kB (+1.12 kB) 19.5 kB (+961 B)
overall 66.7 kB (+3.49 kB) 25.6 kB (+1.1 kB) 23.3 kB (+995 B)

@yyx990803 yyx990803 marked this pull request as draft July 19, 2024 09:38
@yyx990803 yyx990803 marked this pull request as ready for review July 19, 2024 10:03
@yyx990803 yyx990803 merged commit 73ef156 into minor Jul 19, 2024
11 checks passed
@yyx990803 yyx990803 deleted the useId branch July 19, 2024 10:06
@ionutantohi
Copy link

can this be used for key attribute within a v-for and not worrying about creating keys ourselves?

@tuskermanshu
Copy link

After working with useId, I've noticed that it doesn't function as expected when called within asynchronous contexts such as setTimeout, setInterval, or Promise callbacks. I understand that this is due to the current implementation relying on getCurrentInstance(), which returns null in these scenarios.
While I appreciate the design considerations that likely led to this implementation, I'm curious about the rationale behind not providing built-in support for generating IDs in asynchronous environments. Some questions I have:

Was this a deliberate design decision? If so, could you share the reasoning behind it?
Are there any recommended patterns or best practices for generating unique IDs in asynchronous contexts while still maintaining the benefits of useId (such as SSR compatibility and component scope)?
Are there plans to extend useId functionality to work in async environments in future releases?
If extending useId isn't planned, could you provide guidance on implementing a similar functionality that works across both synchronous and asynchronous contexts?

@tuskermanshu
Copy link

After working with useId, I've noticed that it doesn't function as expected when called within asynchronous contexts such as setTimeout, setInterval, or Promise callbacks. I understand that this is due to the current implementation relying on getCurrentInstance(), which returns null in these scenarios. While I appreciate the design considerations that likely led to this implementation, I'm curious about the rationale behind not providing built-in support for generating IDs in asynchronous environments. Some questions I have:

Was this a deliberate design decision? If so, could you share the reasoning behind it? Are there any recommended patterns or best practices for generating unique IDs in asynchronous contexts while still maintaining the benefits of useId (such as SSR compatibility and component scope)? Are there plans to extend useId functionality to work in async environments in future releases? If extending useId isn't planned, could you provide guidance on implementing a similar functionality that works across both synchronous and asynchronous contexts?

Code Example

Here's a simple example to illustrate the issue:

<script setup>
import { ref, onMounted } from 'vue'
import { useId } from 'vue'

const message = ref('')

onMounted(() => {
  const id = useId() // This works fine
  message.value = `Initial ID: ${id}`

  setTimeout(() => {
    const asyncId = useId() // This fails
    message.value = `Async ID: ${asyncId}` // asyncId is undefined
  }, 1000)
})
</script>

<template>
  <div>{{ message }}</div>
</template>

@dr46ul
Copy link

dr46ul commented Sep 5, 2024

Hello!

I’m currently working on updating my Nuxt project to the latest versions of its dependencies, specifically [email protected] and [email protected]. However, after installing these updates, I’ve encountered some issues: several of my tests are failing, and I’m getting a TypeScript error that says:

Type 'string | undefined' is not assignable to type 'string'.

This error appears wherever I use the useId() function. Upon further investigation, I discovered that the type definition indicates this function might return undefined, which is causing problems in areas where I need guaranteed IDs.

image

I’m curious if this behavior is intentional or just an oversight. It seems unusual for a function designed to generate unique IDs to potentially return undefined. What do you think?

@sebbayer
Copy link

sebbayer commented Sep 5, 2024

Thanks for implementing the useID function! This is a huge improvement for SSR. I have created an issue for a problem I found with the current implementation: #11828

@s3xysteak
Copy link

@dadaguai-git This simple change can generate unique IDs in asynchronous contexts. This api design maybe is intended to be consistent with React's useId.

/** useId */
export function useIdCreator() {
  const i = getCurrentInstance()
  if (i) {
    // return (i.appContext.config.idPrefix || 'v') + ':' + i.ids[0] + i.ids[1]++
    return () => (i.appContext.config.idPrefix || 'v') + ':' + i.ids[0] + i.ids[1]++
  } else if (__DEV__) {
    warn(
      `useId() is called when there is no active component ` +
        `instance to be associated with.`,
    )
  }
}

Comment on lines 7 to 17
export function useId() {
const i = getCurrentInstance()
if (i) {
return (i.appContext.config.idPrefix || 'v') + i.ids[0] + ':' + i.ids[1]++
} else if (__DEV__) {
warn(
`useId() is called when there is no active component ` +
`instance to be associated with.`,
)
}
}

Choose a reason for hiding this comment

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

@yyx990803 is there any good reason to not throw an error in case i == false ?
The code as it is right now results in return type of string | undefined.

Sure, a user might call useId outside a component, but I feel like this should just produce an error.
I am having the same problem as @dr46ul describes here

Copy link
Member Author

Choose a reason for hiding this comment

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

None of the APIs that require an active currentInstance throws. This avoids breaking the entire user experience in unexpected cases and leaves the option to throw to the developer.

Copy link

@NiklasBeierl NiklasBeierl Sep 14, 2024

Choose a reason for hiding this comment

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

I'd argue that not throwing in a situation where an active currentInstance is required constitutes "silent failure". But then again console warnings are issued and I might have spent too much time in python-land recently to appreciate avoiding a throw. 😂

Thanks for explaining! 👍

Copy link

@adamdehaven adamdehaven Sep 22, 2024

Choose a reason for hiding this comment

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

Is there a reason then it cannot return an empty string, (or some type of string) rather than undefined?

The string | undefined return type causes downstream apps (including Nuxt) to have to do something like useId()! or YOLO cast const myId = useId() as string to avoid having to account for undefined even when the dev knows they are calling it properly.

Copy link

@NiklasBeierl NiklasBeierl Sep 23, 2024

Choose a reason for hiding this comment

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

I guess yolo-casting ist he way to go if you know you are calling it properly? (i.e. inside a component).

I now actually sort of appreciate this Situation since I was calling useId inside a composable and handling this situation explicitly (with a throw) prevents missunderstandings like trying to use it in a store. (But there are other composables that could be used in a store).

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.

8 participants