mirror of https://github.com/vuejs/core.git
feat(runtime-core): useId() (#11404)
This commit is contained in:
parent
3f8cbb2379
commit
73ef1561f6
|
|
@ -0,0 +1,242 @@
|
||||||
|
/**
|
||||||
|
* @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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -15,6 +15,7 @@ import { ref } from '@vue/reactivity'
|
||||||
import { ErrorCodes, handleError } from './errorHandling'
|
import { ErrorCodes, handleError } from './errorHandling'
|
||||||
import { isKeepAlive } from './components/KeepAlive'
|
import { isKeepAlive } from './components/KeepAlive'
|
||||||
import { queueJob } from './scheduler'
|
import { queueJob } from './scheduler'
|
||||||
|
import { markAsyncBoundary } from './helpers/useId'
|
||||||
|
|
||||||
export type AsyncComponentResolveResult<T = Component> = T | { default: T } // es modules
|
export type AsyncComponentResolveResult<T = Component> = T | { default: T } // es modules
|
||||||
|
|
||||||
|
|
@ -157,6 +158,8 @@ export function defineAsyncComponent<
|
||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
markAsyncBoundary(instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loaded = ref(false)
|
const loaded = ref(false)
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,11 @@ export interface AppConfig {
|
||||||
* But in some cases, e.g. SSR, throwing might be more desirable.
|
* But in some cases, e.g. SSR, throwing might be more desirable.
|
||||||
*/
|
*/
|
||||||
throwUnhandledErrorInProduction?: boolean
|
throwUnhandledErrorInProduction?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix for all useId() calls within this app
|
||||||
|
*/
|
||||||
|
idPrefix?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppContext {
|
export interface AppContext {
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ import type { SuspenseProps } from './components/Suspense'
|
||||||
import type { KeepAliveProps } from './components/KeepAlive'
|
import type { KeepAliveProps } from './components/KeepAlive'
|
||||||
import type { BaseTransitionProps } from './components/BaseTransition'
|
import type { BaseTransitionProps } from './components/BaseTransition'
|
||||||
import type { DefineComponent } from './apiDefineComponent'
|
import type { DefineComponent } from './apiDefineComponent'
|
||||||
|
import { markAsyncBoundary } from './helpers/useId'
|
||||||
|
|
||||||
export type Data = Record<string, unknown>
|
export type Data = Record<string, unknown>
|
||||||
|
|
||||||
|
|
@ -356,6 +357,13 @@ export interface ComponentInternalInstance {
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
provides: Data
|
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
|
* Tracking reactive effects (e.g. watchers) associated with this component
|
||||||
* so that they can be automatically stopped on component unmount
|
* so that they can be automatically stopped on component unmount
|
||||||
|
|
@ -619,6 +627,7 @@ export function createComponentInstance(
|
||||||
withProxy: null,
|
withProxy: null,
|
||||||
|
|
||||||
provides: parent ? parent.provides : Object.create(appContext.provides),
|
provides: parent ? parent.provides : Object.create(appContext.provides),
|
||||||
|
ids: parent ? parent.ids : ['', 0, 0],
|
||||||
accessCache: null!,
|
accessCache: null!,
|
||||||
renderCache: [],
|
renderCache: [],
|
||||||
|
|
||||||
|
|
@ -862,6 +871,8 @@ function setupStatefulComponent(
|
||||||
reset()
|
reset()
|
||||||
|
|
||||||
if (isPromise(setupResult)) {
|
if (isPromise(setupResult)) {
|
||||||
|
// async setup, mark as async boundary for useId()
|
||||||
|
markAsyncBoundary(instance)
|
||||||
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
|
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
|
||||||
if (isSSR) {
|
if (isSSR) {
|
||||||
// return the promise so server-renderer can wait on it
|
// return the promise so server-renderer can wait on it
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ import {
|
||||||
type ComponentTypeEmits,
|
type ComponentTypeEmits,
|
||||||
normalizePropsOrEmits,
|
normalizePropsOrEmits,
|
||||||
} from './apiSetupHelpers'
|
} from './apiSetupHelpers'
|
||||||
|
import { markAsyncBoundary } from './helpers/useId'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for declaring custom options.
|
* Interface for declaring custom options.
|
||||||
|
|
@ -771,6 +772,10 @@ export function applyOptions(instance: ComponentInternalInstance) {
|
||||||
) {
|
) {
|
||||||
instance.filters = filters
|
instance.filters = filters
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (__SSR__ && serverPrefetch) {
|
||||||
|
markAsyncBoundary(instance)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveInjections(
|
export function resolveInjections(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import {
|
||||||
|
type ComponentInternalInstance,
|
||||||
|
getCurrentInstance,
|
||||||
|
} from '../component'
|
||||||
|
import { warn } from '../warning'
|
||||||
|
|
||||||
|
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.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There are 3 types of async boundaries:
|
||||||
|
* - async components
|
||||||
|
* - components with async setup()
|
||||||
|
* - components with serverPrefetch
|
||||||
|
*/
|
||||||
|
export function markAsyncBoundary(instance: ComponentInternalInstance) {
|
||||||
|
instance.ids = [instance.ids[0] + instance.ids[2]++ + '-', 0, 0]
|
||||||
|
}
|
||||||
|
|
@ -63,6 +63,7 @@ export { defineAsyncComponent } from './apiAsyncComponent'
|
||||||
export { useAttrs, useSlots } from './apiSetupHelpers'
|
export { useAttrs, useSlots } from './apiSetupHelpers'
|
||||||
export { useModel } from './helpers/useModel'
|
export { useModel } from './helpers/useModel'
|
||||||
export { useTemplateRef } from './helpers/useTemplateRef'
|
export { useTemplateRef } from './helpers/useTemplateRef'
|
||||||
|
export { useId } from './helpers/useId'
|
||||||
|
|
||||||
// <script setup> API ----------------------------------------------------------
|
// <script setup> API ----------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue