]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(hydration): skip lazy hydration for patched components (#13283)
authoredison <daiwei521@126.com>
Fri, 16 May 2025 00:22:01 +0000 (08:22 +0800)
committerGitHub <noreply@github.com>
Fri, 16 May 2025 00:22:01 +0000 (08:22 +0800)
close #13255

packages/runtime-core/src/apiAsyncComponent.ts
packages/vue/__tests__/e2e/hydration-strat-media.html
packages/vue/__tests__/e2e/hydrationStrategies.spec.ts

index 199b451f66ffcee4b781849d090c3248d7e786f4..cb675f06e432f808e0f45108251bba73db09f803 100644 (file)
@@ -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) {
index c04cdb2a7838549e8a01489651b8f2b4db80c243..954a73d0467c79f60a6f74b7a19b6a881af9c561 100644 (file)
   } = 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')
 </script>
index 69934d9591e8b10f71cd00aafe059c193175e93a..d792edf1960cd4b390535eeaf33ec57e6c150a3f 100644 (file)
@@ -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)