fix(nuxt): avoid premature hydration when using async layouts (#22198)

This commit is contained in:
Daniel Roe 2023-07-19 07:55:53 +01:00 committed by GitHub
parent 449a01526a
commit 5b409f8579
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 13 deletions

View File

@ -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
)
}
}
})

View File

@ -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')
}

View File

@ -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')

View 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>