mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-29 17:07:22 +00:00
feat: add hydration emit for callbacks
This commit is contained in:
parent
373e223268
commit
c0844b902e
@ -11,10 +11,12 @@ export const createLazyIOComponent = (loader: AsyncComponentLoader) => {
|
|||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup (props, { attrs }) {
|
emits: ['hydrated'],
|
||||||
|
setup (props, { attrs, emit }) {
|
||||||
|
const hydrated = () => { emit('hydrated') }
|
||||||
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnVisible(props.hydrate as IntersectionObserverInit | undefined) })
|
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnVisible(props.hydrate as IntersectionObserverInit | undefined) })
|
||||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -29,14 +31,16 @@ export const createLazyNetworkComponent = (loader: AsyncComponentLoader) => {
|
|||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup (props, { attrs }) {
|
emits: ['hydrated'],
|
||||||
|
setup (props, { attrs, emit }) {
|
||||||
|
const hydrated = () => { emit('hydrated') }
|
||||||
if (props.hydrate === 0) {
|
if (props.hydrate === 0) {
|
||||||
const comp = defineAsyncComponent(loader)
|
const comp = defineAsyncComponent(loader)
|
||||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||||
}
|
}
|
||||||
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnIdle(props.hydrate) })
|
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnIdle(props.hydrate) })
|
||||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -52,11 +56,13 @@ export const createLazyEventComponent = (loader: AsyncComponentLoader) => {
|
|||||||
default: 'mouseover',
|
default: 'mouseover',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup (props, { attrs }) {
|
emits: ['hydrated'],
|
||||||
|
setup (props, { attrs, emit }) {
|
||||||
|
const hydrated = () => { emit('hydrated') }
|
||||||
// @ts-expect-error Cannot type HTMLElementEventMap in props
|
// @ts-expect-error Cannot type HTMLElementEventMap in props
|
||||||
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnInteraction(props.hydrate) })
|
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnInteraction(props.hydrate) })
|
||||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -72,10 +78,12 @@ export const createLazyMediaComponent = (loader: AsyncComponentLoader) => {
|
|||||||
default: '(min-width: 1px)',
|
default: '(min-width: 1px)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup (props, { attrs }) {
|
emits: ['hydrated'],
|
||||||
|
setup (props, { attrs, emit }) {
|
||||||
|
const hydrated = () => { emit('hydrated') }
|
||||||
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnMediaQuery(props.hydrate) })
|
const comp = defineAsyncComponent({ loader, hydrate: hydrateOnMediaQuery(props.hydrate) })
|
||||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -91,18 +99,20 @@ export const createLazyIfComponent = (loader: AsyncComponentLoader) => {
|
|||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup (props, { attrs }) {
|
emits: ['hydrated'],
|
||||||
|
setup (props, { attrs, emit }) {
|
||||||
|
const hydrated = () => { emit('hydrated') }
|
||||||
if (props.hydrate) {
|
if (props.hydrate) {
|
||||||
const comp = defineAsyncComponent(loader)
|
const comp = defineAsyncComponent(loader)
|
||||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||||
}
|
}
|
||||||
const strategy: HydrationStrategy = (hydrate) => {
|
const strategy: HydrationStrategy = (hydrate) => {
|
||||||
const unwatch = watch(() => props.hydrate, () => hydrate(), { once: true })
|
const unwatch = watch(() => props.hydrate, () => hydrate(), { once: true })
|
||||||
return () => unwatch()
|
return () => unwatch()
|
||||||
}
|
}
|
||||||
const comp = defineAsyncComponent({ loader, hydrate: strategy })
|
const comp = defineAsyncComponent({ loader, hydrate: strategy })
|
||||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -118,10 +128,12 @@ export const createLazyTimeComponent = (loader: AsyncComponentLoader) => {
|
|||||||
default: 2000,
|
default: 2000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup (props, { attrs }) {
|
emits: ['hydrated'],
|
||||||
|
setup (props, { attrs, emit }) {
|
||||||
|
const hydrated = () => { emit('hydrated') }
|
||||||
if (props.hydrate === 0) {
|
if (props.hydrate === 0) {
|
||||||
const comp = defineAsyncComponent(loader)
|
const comp = defineAsyncComponent(loader)
|
||||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||||
}
|
}
|
||||||
const strategy: HydrationStrategy = (hydrate) => {
|
const strategy: HydrationStrategy = (hydrate) => {
|
||||||
const id = setTimeout(hydrate, props.hydrate)
|
const id = setTimeout(hydrate, props.hydrate)
|
||||||
@ -129,7 +141,7 @@ export const createLazyTimeComponent = (loader: AsyncComponentLoader) => {
|
|||||||
}
|
}
|
||||||
const comp = defineAsyncComponent({ loader, hydrate: strategy })
|
const comp = defineAsyncComponent({ loader, hydrate: strategy })
|
||||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -144,11 +156,13 @@ export const createLazyPromiseComponent = (loader: AsyncComponentLoader) => {
|
|||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup (props, { attrs }) {
|
emits: ['hydrated'],
|
||||||
|
setup (props, { attrs, emit }) {
|
||||||
|
const hydrated = () => { emit('hydrated') }
|
||||||
if (!props.hydrate || typeof props.hydrate.then !== 'function') {
|
if (!props.hydrate || typeof props.hydrate.then !== 'function') {
|
||||||
const comp = defineAsyncComponent(loader)
|
const comp = defineAsyncComponent(loader)
|
||||||
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
// TODO: fix hydration mismatches on Vue's side. The data-allow-mismatch is ideally a temporary solution due to Vue's SSR limitation with hydrated content.
|
||||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||||
}
|
}
|
||||||
const strategy: HydrationStrategy = (hydrate) => {
|
const strategy: HydrationStrategy = (hydrate) => {
|
||||||
// @ts-expect-error TS does not see hydrate as non-null
|
// @ts-expect-error TS does not see hydrate as non-null
|
||||||
@ -156,7 +170,7 @@ export const createLazyPromiseComponent = (loader: AsyncComponentLoader) => {
|
|||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
const comp = defineAsyncComponent({ loader, hydrate: strategy })
|
const comp = defineAsyncComponent({ loader, hydrate: strategy })
|
||||||
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '' }))
|
return () => h(comp, mergeProps(attrs, { 'data-allow-mismatch': '', 'onVnodeMounted': hydrated }))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -115,21 +115,22 @@ export const componentsTypeTemplate = {
|
|||||||
c.island || c.mode === 'server' ? `IslandComponent<${type}>` : type,
|
c.island || c.mode === 'server' ? `IslandComponent<${type}>` : type,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const islandType = 'type IslandComponent<T extends DefineComponent> = T & DefineComponent<{}, {refresh: () => Promise<void>}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, SlotsType<{ fallback: { error: unknown } }>>'
|
const islandType = 'type IslandComponent<T extends DefineComponent> = T & DefineComponent<{}, {refresh: () => Promise<void>}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, SlotsType<{ fallback: { error: unknown } }>>'
|
||||||
|
const delayedType = 'type DelayedComponent<T> = DefineComponent<{hydrate?: T},{},{},{},{},{},{},{hydrated: void}>'
|
||||||
return `
|
return `
|
||||||
import type { DefineComponent, SlotsType } from 'vue'
|
import type { DefineComponent, SlotsType } from 'vue'
|
||||||
${nuxt.options.experimental.componentIslands ? islandType : ''}
|
${nuxt.options.experimental.componentIslands ? islandType : ''}
|
||||||
|
${nuxt.options.experimental.delayedHydration ? delayedType : ''}
|
||||||
interface _GlobalComponents {
|
interface _GlobalComponents {
|
||||||
${componentTypes.map(([pascalName, type]) => ` '${pascalName}': ${type}`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => ` '${pascalName}': ${type}`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => ` 'Lazy${pascalName}': ${type}`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => ` 'Lazy${pascalName}': ${type}`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => ` 'LazyIdle${pascalName}': ${type} & DefineComponent<{hydrate?: number}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => ` 'LazyIdle${pascalName}': ${type} & DelayedComponent<number>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => ` 'LazyTime${pascalName}': ${type} & DefineComponent<{hydrate?: number}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => ` 'LazyTime${pascalName}': ${type} & DelayedComponent<number>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => ` 'LazyPromise${pascalName}': ${type} & DefineComponent<{hydrate?: Promise<unknown>}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => ` 'LazyPromise${pascalName}': ${type} & DelayedComponent<Promise<unknown>>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => ` 'LazyVisible${pascalName}': ${type} & DefineComponent<{hydrate?: Partial<IntersectionObserverInit>}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => ` 'LazyVisible${pascalName}': ${type} & DelayedComponent<IntersectionObserverInit>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => ` 'LazyEvent${pascalName}': ${type} & DefineComponent<{hydrate?: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => ` 'LazyEvent${pascalName}': ${type} & DelayedComponent<keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => ` 'LazyMedia${pascalName}': ${type} & DefineComponent<{hydrate?: string}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => ` 'LazyMedia${pascalName}': ${type} & DelayedComponent<string>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => ` 'LazyIf${pascalName}': ${type} & DefineComponent<{hydrate?: unknown}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => ` 'LazyIf${pascalName}': ${type} & DelayedComponent<unknown>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => ` 'LazyNever${pascalName}': ${type}`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => ` 'LazyNever${pascalName}': ${type}`).join('\n')}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,13 +140,13 @@ declare module 'vue' {
|
|||||||
|
|
||||||
${componentTypes.map(([pascalName, type]) => `export const ${pascalName}: ${type}`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => `export const ${pascalName}: ${type}`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => `export const Lazy${pascalName}: ${type}`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => `export const Lazy${pascalName}: ${type}`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => `export const LazyIdle${pascalName}: ${type} & DefineComponent<{hydrate?: number}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => `export const LazyIdle${pascalName}: ${type} & DelayedComponent<number>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => `export const LazyTime${pascalName}: ${type} & DefineComponent<{hydrate?: number}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => `export const LazyTime${pascalName}: ${type} & DelayedComponent<number>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => `export const LazyPromise${pascalName}: ${type} & DefineComponent<{hydrate?: Promise<unknown>}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => `export const LazyPromise${pascalName}: ${type} & DelayedComponent<Promise<unknown>>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => `export const LazyVisible${pascalName}: ${type} & DefineComponent<{hydrate?: Partial<IntersectionObserverInit>}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => `export const LazyVisible${pascalName}: ${type} & DelayedComponent<IntersectionObserverInit>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => `export const LazyEvent${pascalName}: ${type} & DefineComponent<{hydrate?: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => `export const LazyEvent${pascalName}: ${type} & DelayedComponent<keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap>>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => `export const LazyMedia${pascalName}: ${type} & DefineComponent<{hydrate?: string}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => `export const LazyMedia${pascalName}: ${type} & DelayedComponent<string>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => `export const LazyIf${pascalName}: ${type} & DefineComponent<{hydrate?: unknown}>`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => `export const LazyIf${pascalName}: ${type} & DelayedComponent<unknown>`).join('\n')}
|
||||||
${componentTypes.map(([pascalName, type]) => `export const LazyNever${pascalName}: ${type}`).join('\n')}
|
${componentTypes.map(([pascalName, type]) => `export const LazyNever${pascalName}: ${type}`).join('\n')}
|
||||||
|
|
||||||
export const componentNames: string[]
|
export const componentNames: string[]
|
||||||
|
@ -219,7 +219,6 @@ export default defineUntypedSchema({
|
|||||||
/**
|
/**
|
||||||
* Delayed component hydration
|
* Delayed component hydration
|
||||||
*
|
*
|
||||||
* This enables components to defer hydration until necessary, improving performance by not requesting all resources at once.
|
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
delayedHydration: false,
|
delayedHydration: false,
|
||||||
|
@ -2840,15 +2840,20 @@ describe('lazy import components', () => {
|
|||||||
expect(await page.locator('body').getByText('This should be visible at first with promise!').all()).toHaveLength(0)
|
expect(await page.locator('body').getByText('This should be visible at first with promise!').all()).toHaveLength(0)
|
||||||
})
|
})
|
||||||
it('keeps reactivity with models', async () => {
|
it('keeps reactivity with models', async () => {
|
||||||
const { page } = await renderPage('/lazy-import-components/model')
|
const { page } = await renderPage('/lazy-import-components/model-event')
|
||||||
expect(await page.locator('#count').textContent()).toBe('0')
|
expect(await page.locator('#count').textContent()).toBe('0')
|
||||||
await page.locator('#count').click()
|
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
expect(await page.locator('#count').textContent()).toBe(`${i}`)
|
expect(await page.locator('#count').textContent()).toBe(`${i}`)
|
||||||
await page.locator('#inc').click()
|
await page.locator('#inc').click()
|
||||||
}
|
}
|
||||||
expect(await page.locator('#count').textContent()).toBe('10')
|
expect(await page.locator('#count').textContent()).toBe('10')
|
||||||
})
|
})
|
||||||
|
it('emits hydration events', async () => {
|
||||||
|
const { page, consoleLogs } = await renderPage('/lazy-import-components/model-event')
|
||||||
|
expect(consoleLogs.some(log => log.type === 'log' && log.text === 'Component hydrated')).toBeFalsy()
|
||||||
|
await page.locator('#count').click()
|
||||||
|
expect(consoleLogs.some(log => log.type === 'log' && log.text === 'Component hydrated')).toBeTruthy()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('defineNuxtComponent watch duplicate', () => {
|
describe('defineNuxtComponent watch duplicate', () => {
|
||||||
|
15
test/fixtures/basic/pages/lazy-import-components/model-event.vue
vendored
Normal file
15
test/fixtures/basic/pages/lazy-import-components/model-event.vue
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<LazyEventDelayedModel
|
||||||
|
v-model="model"
|
||||||
|
@hydrated="log"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const model = ref(0)
|
||||||
|
function log () {
|
||||||
|
console.log('Component hydrated')
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,9 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<LazyEventDelayedModel v-model="model" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const model = ref(0)
|
|
||||||
</script>
|
|
Loading…
Reference in New Issue
Block a user