mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-21 21:25:11 +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 { 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({
|
||||
name: 'NuxtLayout',
|
||||
inheritAttrs: false,
|
||||
@ -46,14 +61,16 @@ export default defineComponent({
|
||||
// We avoid rendering layout transition if there is no layout to render
|
||||
return _wrapIf(Transition, hasLayout && transitionProps, {
|
||||
default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, {
|
||||
// @ts-expect-error seems to be an issue in vue types
|
||||
default: () => h(LayoutProvider, {
|
||||
layoutProps: mergeProps(context.attrs, { ref: layoutRef }),
|
||||
key: layout.value,
|
||||
name: layout.value,
|
||||
shouldProvide: !props.name,
|
||||
hasTransition: !!transitionProps
|
||||
}, context.slots)
|
||||
default: () => h(
|
||||
// @ts-expect-error seems to be an issue in vue types
|
||||
LayoutProvider,
|
||||
{
|
||||
layoutProps: mergeProps(context.attrs, { ref: layoutRef }),
|
||||
key: layout.value,
|
||||
name: layout.value,
|
||||
shouldProvide: !props.name,
|
||||
hasTransition: !!transitionProps
|
||||
}, context.slots)
|
||||
})
|
||||
}).default()
|
||||
}
|
||||
@ -112,12 +129,22 @@ const LayoutProvider = defineComponent({
|
||||
}
|
||||
|
||||
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 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',
|
||||
getContents ({ app }) {
|
||||
const layoutsObject = genObjectFromRawEntries(Object.values(app.layouts).map(({ name, file }) => {
|
||||
return [name, `defineAsyncComponent(${genDynamicImport(file, { interopDefault: true })})`]
|
||||
return [name, genDynamicImport(file, { interopDefault: true })]
|
||||
}))
|
||||
return [
|
||||
'import { defineAsyncComponent } from \'vue\'',
|
||||
`export default ${layoutsObject}`
|
||||
].join('\n')
|
||||
}
|
||||
|
@ -1020,7 +1020,7 @@ describe('deferred app suspense resolve', () => {
|
||||
await page.goto(url(path))
|
||||
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)))
|
||||
|
||||
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 () => {
|
||||
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 () => {
|
||||
const page = await createPage('/hydration/spa-redirection/start')
|
||||
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