diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 4a9e0fac2b6..43af0583ce4 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -1160,6 +1160,69 @@ describe('SSR hydration', () => { ) }) + // #13510 + test('update async component after parent mount before async component resolve', async () => { + const Comp = { + props: ['toggle'], + render(this: any) { + return h('h1', [ + this.toggle ? 'Async component' : 'Updated async component', + ]) + }, + } + let serverResolve: any + let AsyncComp = defineAsyncComponent( + () => + new Promise(r => { + serverResolve = r + }), + ) + + const toggle = ref(true) + const App = { + setup() { + onMounted(() => { + // change state, after mount and before async component resolve + nextTick(() => (toggle.value = false)) + }) + + return () => { + return h(AsyncComp, { toggle: toggle.value }) + } + }, + } + + // server render + const htmlPromise = renderToString(h(App)) + serverResolve(Comp) + const html = await htmlPromise + expect(html).toMatchInlineSnapshot(`"

Async component

"`) + + // hydration + let clientResolve: any + AsyncComp = defineAsyncComponent( + () => + new Promise(r => { + clientResolve = r + }), + ) + + const container = document.createElement('div') + container.innerHTML = html + createSSRApp(App).mount(container) + + // resolve + clientResolve(Comp) + await new Promise(r => setTimeout(r)) + + // prevent lazy hydration since the component has been patched + expect('Skipping lazy hydration for component').toHaveBeenWarned() + expect(`Hydration node mismatch`).not.toHaveBeenWarned() + expect(container.innerHTML).toMatchInlineSnapshot( + `"

Updated async component

"`, + ) + }) + test('hydrate safely when property used by async setup changed before render', async () => { const toggle = ref(true) diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts index cb675f06e43..25e6fa481ca 100644 --- a/packages/runtime-core/src/apiAsyncComponent.ts +++ b/packages/runtime-core/src/apiAsyncComponent.ts @@ -123,28 +123,28 @@ export function defineAsyncComponent< __asyncHydrate(el, instance, hydrate) { let patched = false + ;(instance.bu || (instance.bu = [])).push(() => (patched = true)) + const performHydrate = () => { + // skip hydration if the component has been patched + if (__DEV__ && patched) { + warn( + `Skipping lazy hydration for component '${getComponentName(resolvedComp!) || resolvedComp!.__file}': ` + + `it was updated before lazy hydration performed.`, + ) + return + } + hydrate() + } const doHydrate = hydrateStrategy ? () => { - const performHydrate = () => { - // skip hydration if the component has been patched - if (__DEV__ && patched) { - warn( - `Skipping lazy hydration for component '${getComponentName(resolvedComp!)}': ` + - `it was updated before lazy hydration performed.`, - ) - return - } - hydrate() - } const teardown = hydrateStrategy(performHydrate, cb => forEachElement(el, cb), ) if (teardown) { ;(instance.bum || (instance.bum = [])).push(teardown) } - ;(instance.u || (instance.u = [])).push(() => (patched = true)) } - : hydrate + : performHydrate if (resolvedComp) { doHydrate() } else {