diff --git a/packages/runtime-core/src/apiAsyncComponent.ts b/packages/runtime-core/src/apiAsyncComponent.ts index 199b451f66f..cb675f06e43 100644 --- a/packages/runtime-core/src/apiAsyncComponent.ts +++ b/packages/runtime-core/src/apiAsyncComponent.ts @@ -4,6 +4,7 @@ import { type ComponentOptions, type ConcreteComponent, currentInstance, + getComponentName, isInSSRComponentSetup, } from './component' import { isFunction, isObject } from '@vue/shared' @@ -121,14 +122,27 @@ export function defineAsyncComponent< __asyncLoader: load, __asyncHydrate(el, instance, hydrate) { + let patched = false const doHydrate = hydrateStrategy ? () => { - const teardown = hydrateStrategy(hydrate, cb => + 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 if (resolvedComp) { diff --git a/packages/vue/__tests__/e2e/hydration-strat-media.html b/packages/vue/__tests__/e2e/hydration-strat-media.html index c04cdb2a783..954a73d0467 100644 --- a/packages/vue/__tests__/e2e/hydration-strat-media.html +++ b/packages/vue/__tests__/e2e/hydration-strat-media.html @@ -15,13 +15,17 @@ } = Vue const Comp = { - setup() { + props: { + value: Boolean, + }, + setup(props) { const count = ref(0) onMounted(() => { console.log('hydrated') window.isHydrated = true }) return () => { + props.value return h('button', { onClick: () => count.value++ }, count.value) } }, @@ -37,7 +41,9 @@ onMounted(() => { window.isRootMounted = true }) - return () => h(AsyncComp) + + const show = (window.show = ref(true)) + return () => h(AsyncComp, { value: show.value }) }, }).mount('#app') diff --git a/packages/vue/__tests__/e2e/hydrationStrategies.spec.ts b/packages/vue/__tests__/e2e/hydrationStrategies.spec.ts index 69934d9591e..d792edf1960 100644 --- a/packages/vue/__tests__/e2e/hydrationStrategies.spec.ts +++ b/packages/vue/__tests__/e2e/hydrationStrategies.spec.ts @@ -86,6 +86,36 @@ describe('async component hydration strategies', () => { await assertHydrationSuccess() }) + // #13255 + test('media query (patched before hydration)', async () => { + const spy = vi.fn() + const currentPage = page() + currentPage.on('pageerror', spy) + + const warn: any[] = [] + currentPage.on('console', e => warn.push(e.text())) + + await goToCase('media') + await page().waitForFunction(() => window.isRootMounted) + expect(await page().evaluate(() => window.isHydrated)).toBe(false) + + // patch + await page().evaluate(() => (window.show.value = false)) + await click('button') + expect(await text('button')).toBe('1') + + // resize + await page().setViewport({ width: 400, height: 600 }) + await page().waitForFunction(() => window.isHydrated) + await assertHydrationSuccess('2') + + expect(spy).toBeCalledTimes(0) + currentPage.off('pageerror', spy) + expect( + warn.some(w => w.includes('Skipping lazy hydration for component')), + ).toBe(true) + }) + test('interaction', async () => { await goToCase('interaction') await page().waitForFunction(() => window.isRootMounted)