]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(hydration): prevent lazy hydration for updated components (#13511)
authoredison <daiwei521@126.com>
Wed, 23 Jul 2025 00:36:47 +0000 (08:36 +0800)
committerGitHub <noreply@github.com>
Wed, 23 Jul 2025 00:36:47 +0000 (08:36 +0800)
close #13510

packages/runtime-core/__tests__/hydration.spec.ts
packages/runtime-core/src/apiAsyncComponent.ts

index 4a9e0fac2b6e8f1f69215d47219556d5bcbb7749..43af0583ce40098f99cf34f526ae8cb54c86ce88 100644 (file)
@@ -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(`"<h1>Async component</h1>"`)
+
+    // 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(
+      `"<h1>Updated async component</h1>"`,
+    )
+  })
+
   test('hydrate safely when property used by async setup changed before render', async () => {
     const toggle = ref(true)
 
index cb675f06e432f808e0f45108251bba73db09f803..ab4ab51b66adea62c38e0bb7a4e602460d0a56be 100644 (file)
@@ -123,28 +123,30 @@ 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 (patched) {
+          if (__DEV__) {
+            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 {