mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +00:00
fix(nuxt): avoid premature hydration when using async layouts (#22198)
This commit is contained in:
parent
449a01526a
commit
5b409f8579
@ -13,6 +13,21 @@ import layouts from '#build/layouts'
|
|||||||
import { appLayoutTransition as defaultLayoutTransition } from '#build/nuxt.config.mjs'
|
import { appLayoutTransition as defaultLayoutTransition } from '#build/nuxt.config.mjs'
|
||||||
import { useNuxtApp } from '#app'
|
import { useNuxtApp } from '#app'
|
||||||
|
|
||||||
|
// TODO: revert back to defineAsyncComponent when https://github.com/vuejs/core/issues/6638 is resolved
|
||||||
|
const LayoutLoader = defineComponent({
|
||||||
|
name: 'LayoutLoader',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
layoutProps: Object
|
||||||
|
},
|
||||||
|
async setup (props, context) {
|
||||||
|
const LayoutComponent = await layouts[props.name]().then((r: any) => r.default || r)
|
||||||
|
|
||||||
|
return () => h(LayoutComponent, props.layoutProps, context.slots)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'NuxtLayout',
|
name: 'NuxtLayout',
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
@ -46,14 +61,16 @@ export default defineComponent({
|
|||||||
// We avoid rendering layout transition if there is no layout to render
|
// We avoid rendering layout transition if there is no layout to render
|
||||||
return _wrapIf(Transition, hasLayout && transitionProps, {
|
return _wrapIf(Transition, hasLayout && transitionProps, {
|
||||||
default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, {
|
default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, {
|
||||||
// @ts-expect-error seems to be an issue in vue types
|
default: () => h(
|
||||||
default: () => h(LayoutProvider, {
|
// @ts-expect-error seems to be an issue in vue types
|
||||||
layoutProps: mergeProps(context.attrs, { ref: layoutRef }),
|
LayoutProvider,
|
||||||
key: layout.value,
|
{
|
||||||
name: layout.value,
|
layoutProps: mergeProps(context.attrs, { ref: layoutRef }),
|
||||||
shouldProvide: !props.name,
|
key: layout.value,
|
||||||
hasTransition: !!transitionProps
|
name: layout.value,
|
||||||
}, context.slots)
|
shouldProvide: !props.name,
|
||||||
|
hasTransition: !!transitionProps
|
||||||
|
}, context.slots)
|
||||||
})
|
})
|
||||||
}).default()
|
}).default()
|
||||||
}
|
}
|
||||||
@ -112,12 +129,22 @@ const LayoutProvider = defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.dev && process.client && props.hasTransition) {
|
if (process.dev && process.client && props.hasTransition) {
|
||||||
vnode = h(layouts[name], props.layoutProps, context.slots)
|
vnode = h(
|
||||||
|
// @ts-expect-error seems to be an issue in vue types
|
||||||
|
LayoutLoader,
|
||||||
|
{ key: name, layoutProps: props.layoutProps, name },
|
||||||
|
context.slots
|
||||||
|
)
|
||||||
|
|
||||||
return vnode
|
return vnode
|
||||||
}
|
}
|
||||||
|
|
||||||
return h(layouts[name], props.layoutProps, context.slots)
|
return h(
|
||||||
|
// @ts-expect-error seems to be an issue in vue types
|
||||||
|
LayoutLoader,
|
||||||
|
{ key: name, layoutProps: props.layoutProps, name },
|
||||||
|
context.slots
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -175,10 +175,9 @@ export const layoutTemplate: NuxtTemplate<TemplateContext> = {
|
|||||||
filename: 'layouts.mjs',
|
filename: 'layouts.mjs',
|
||||||
getContents ({ app }) {
|
getContents ({ app }) {
|
||||||
const layoutsObject = genObjectFromRawEntries(Object.values(app.layouts).map(({ name, file }) => {
|
const layoutsObject = genObjectFromRawEntries(Object.values(app.layouts).map(({ name, file }) => {
|
||||||
return [name, `defineAsyncComponent(${genDynamicImport(file, { interopDefault: true })})`]
|
return [name, genDynamicImport(file, { interopDefault: true })]
|
||||||
}))
|
}))
|
||||||
return [
|
return [
|
||||||
'import { defineAsyncComponent } from \'vue\'',
|
|
||||||
`export default ${layoutsObject}`
|
`export default ${layoutsObject}`
|
||||||
].join('\n')
|
].join('\n')
|
||||||
}
|
}
|
||||||
|
@ -1020,7 +1020,7 @@ describe('deferred app suspense resolve', () => {
|
|||||||
await page.goto(url(path))
|
await page.goto(url(path))
|
||||||
await page.waitForLoadState('networkidle')
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
// Wait for all pending micro ticks to be cleared in case hydration haven't finished yet.
|
// Wait for all pending micro ticks to be cleared in case hydration hasn't finished yet.
|
||||||
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
|
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
|
||||||
|
|
||||||
const hydrationLogs = logs.filter(log => log.includes('isHydrating'))
|
const hydrationLogs = logs.filter(log => log.includes('isHydrating'))
|
||||||
@ -1034,6 +1034,16 @@ describe('deferred app suspense resolve', () => {
|
|||||||
it('should wait for all suspense instance on initial hydration', async () => {
|
it('should wait for all suspense instance on initial hydration', async () => {
|
||||||
await behaviour('/internal-layout/async-parent/child')
|
await behaviour('/internal-layout/async-parent/child')
|
||||||
})
|
})
|
||||||
|
it('should wait for suspense in parent layout', async () => {
|
||||||
|
const page = await createPage('/hydration/layout')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
// Wait for all pending micro ticks to be cleared in case hydration hasn't finished yet.
|
||||||
|
await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 10)))
|
||||||
|
|
||||||
|
const html = await page.getByRole('document').innerHTML()
|
||||||
|
expect(html).toContain('Tests whether hydration is properly resolved within an async layout')
|
||||||
|
})
|
||||||
it('should fully hydrate even if there is a redirection on a page with `ssr: false`', async () => {
|
it('should fully hydrate even if there is a redirection on a page with `ssr: false`', async () => {
|
||||||
const page = await createPage('/hydration/spa-redirection/start')
|
const page = await createPage('/hydration/spa-redirection/start')
|
||||||
await page.waitForLoadState('networkidle')
|
await page.waitForLoadState('networkidle')
|
||||||
|
18
test/fixtures/basic/pages/hydration/layout.vue
vendored
Normal file
18
test/fixtures/basic/pages/hydration/layout.vue
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'custom-async'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (process.client && !useNuxtApp().isHydrating) {
|
||||||
|
throw createError({
|
||||||
|
fatal: true,
|
||||||
|
message: '`useNuxtApp().isHydrating` is false by the time we run page setup'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
Tests whether hydration is properly resolved within an async layout
|
||||||
|
</div>
|
||||||
|
</template>
|
Loading…
Reference in New Issue
Block a user