--- /dev/null
+/**
+ * @vitest-environment jsdom
+ */
+import {
+ type App,
+ Suspense,
+ createApp,
+ defineAsyncComponent,
+ defineComponent,
+ h,
+ useId,
+} from 'vue'
+import { renderToString } from '@vue/server-renderer'
+
+type TestCaseFactory = () => [App, Promise<any>[]]
+
+async function runOnClient(factory: TestCaseFactory) {
+ const [app, deps] = factory()
+ const root = document.createElement('div')
+ app.mount(root)
+ await Promise.all(deps)
+ await promiseWithDelay(null, 0)
+ return root.innerHTML
+}
+
+async function runOnServer(factory: TestCaseFactory) {
+ const [app, _] = factory()
+ return (await renderToString(app))
+ .replace(/<!--[\[\]]-->/g, '') // remove fragment wrappers
+ .trim()
+}
+
+async function getOutput(factory: TestCaseFactory) {
+ const clientResult = await runOnClient(factory)
+ const serverResult = await runOnServer(factory)
+ expect(serverResult).toBe(clientResult)
+ return clientResult
+}
+
+function promiseWithDelay(res: any, delay: number) {
+ return new Promise<any>(r => {
+ setTimeout(() => r(res), delay)
+ })
+}
+
+const BasicComponentWithUseId = defineComponent({
+ setup() {
+ const id1 = useId()
+ const id2 = useId()
+ return () => [id1, ' ', id2]
+ },
+})
+
+describe('useId', () => {
+ test('basic', async () => {
+ expect(
+ await getOutput(() => {
+ const app = createApp(BasicComponentWithUseId)
+ return [app, []]
+ }),
+ ).toBe('v:0 v:1')
+ })
+
+ test('with config.idPrefix', async () => {
+ expect(
+ await getOutput(() => {
+ const app = createApp(BasicComponentWithUseId)
+ app.config.idPrefix = 'foo'
+ return [app, []]
+ }),
+ ).toBe('foo:0 foo:1')
+ })
+
+ test('async component', async () => {
+ const factory = (
+ delay1: number,
+ delay2: number,
+ ): ReturnType<TestCaseFactory> => {
+ const p1 = promiseWithDelay(BasicComponentWithUseId, delay1)
+ const p2 = promiseWithDelay(BasicComponentWithUseId, delay2)
+ const AsyncOne = defineAsyncComponent(() => p1)
+ const AsyncTwo = defineAsyncComponent(() => p2)
+ const app = createApp({
+ setup() {
+ const id1 = useId()
+ const id2 = useId()
+ return () => [id1, ' ', id2, ' ', h(AsyncOne), ' ', h(AsyncTwo)]
+ },
+ })
+ return [app, [p1, p2]]
+ }
+
+ const expected =
+ 'v:0 v:1 ' + // root
+ 'v:0-0 v:0-1 ' + // inside first async subtree
+ 'v:1-0 v:1-1' // inside second async subtree
+ // assert different async resolution order does not affect id stable-ness
+ expect(await getOutput(() => factory(10, 20))).toBe(expected)
+ expect(await getOutput(() => factory(20, 10))).toBe(expected)
+ })
+
+ test('serverPrefetch', async () => {
+ const factory = (
+ delay1: number,
+ delay2: number,
+ ): ReturnType<TestCaseFactory> => {
+ const p1 = promiseWithDelay(null, delay1)
+ const p2 = promiseWithDelay(null, delay2)
+
+ const SPOne = defineComponent({
+ async serverPrefetch() {
+ await p1
+ },
+ render() {
+ return h(BasicComponentWithUseId)
+ },
+ })
+
+ const SPTwo = defineComponent({
+ async serverPrefetch() {
+ await p2
+ },
+ render() {
+ return h(BasicComponentWithUseId)
+ },
+ })
+
+ const app = createApp({
+ setup() {
+ const id1 = useId()
+ const id2 = useId()
+ return () => [id1, ' ', id2, ' ', h(SPOne), ' ', h(SPTwo)]
+ },
+ })
+ return [app, [p1, p2]]
+ }
+
+ const expected =
+ 'v:0 v:1 ' + // root
+ 'v:0-0 v:0-1 ' + // inside first async subtree
+ 'v:1-0 v:1-1' // inside second async subtree
+ // assert different async resolution order does not affect id stable-ness
+ expect(await getOutput(() => factory(10, 20))).toBe(expected)
+ expect(await getOutput(() => factory(20, 10))).toBe(expected)
+ })
+
+ test('async setup()', async () => {
+ const factory = (
+ delay1: number,
+ delay2: number,
+ ): ReturnType<TestCaseFactory> => {
+ const p1 = promiseWithDelay(null, delay1)
+ const p2 = promiseWithDelay(null, delay2)
+
+ const ASOne = defineComponent({
+ async setup() {
+ await p1
+ return {}
+ },
+ render() {
+ return h(BasicComponentWithUseId)
+ },
+ })
+
+ const ASTwo = defineComponent({
+ async setup() {
+ await p2
+ return {}
+ },
+ render() {
+ return h(BasicComponentWithUseId)
+ },
+ })
+
+ const app = createApp({
+ setup() {
+ const id1 = useId()
+ const id2 = useId()
+ return () =>
+ h(Suspense, null, {
+ default: h('div', [id1, ' ', id2, ' ', h(ASOne), ' ', h(ASTwo)]),
+ })
+ },
+ })
+ return [app, [p1, p2]]
+ }
+
+ const expected =
+ '<div>' +
+ 'v:0 v:1 ' + // root
+ 'v:0-0 v:0-1 ' + // inside first async subtree
+ 'v:1-0 v:1-1' + // inside second async subtree
+ '</div>'
+ // assert different async resolution order does not affect id stable-ness
+ expect(await getOutput(() => factory(10, 20))).toBe(expected)
+ expect(await getOutput(() => factory(20, 10))).toBe(expected)
+ })
+
+ test('deep nested', async () => {
+ const factory = (): ReturnType<TestCaseFactory> => {
+ const p = Promise.resolve()
+ const One = {
+ async setup() {
+ const id = useId()
+ await p
+ return () => [id, ' ', h(Two), ' ', h(Three)]
+ },
+ }
+ const Two = {
+ async setup() {
+ const id = useId()
+ await p
+ return () => [id, ' ', h(Three), ' ', h(Three)]
+ },
+ }
+ const Three = {
+ async setup() {
+ const id = useId()
+ return () => id
+ },
+ }
+ const app = createApp({
+ setup() {
+ return () =>
+ h(Suspense, null, {
+ default: h(One),
+ })
+ },
+ })
+ return [app, [p]]
+ }
+
+ const expected =
+ 'v:0 ' + // One
+ 'v:0-0 ' + // Two
+ 'v:0-0-0 v:0-0-1 ' + // Three + Three nested in Two
+ 'v:0-1' // Three after Two
+ // assert different async resolution order does not affect id stable-ness
+ expect(await getOutput(() => factory())).toBe(expected)
+ expect(await getOutput(() => factory())).toBe(expected)
+ })
+})
import type { KeepAliveProps } from './components/KeepAlive'
import type { BaseTransitionProps } from './components/BaseTransition'
import type { DefineComponent } from './apiDefineComponent'
+import { markAsyncBoundary } from './helpers/useId'
export type Data = Record<string, unknown>
* @internal
*/
provides: Data
+ /**
+ * for tracking useId()
+ * first element is the current boundary prefix
+ * second number is the index of the useId call within that boundary
+ * @internal
+ */
+ ids: [string, number, number]
/**
* Tracking reactive effects (e.g. watchers) associated with this component
* so that they can be automatically stopped on component unmount
withProxy: null,
provides: parent ? parent.provides : Object.create(appContext.provides),
+ ids: parent ? parent.ids : ['', 0, 0],
accessCache: null!,
renderCache: [],
reset()
if (isPromise(setupResult)) {
+ // async setup, mark as async boundary for useId()
+ markAsyncBoundary(instance)
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it