]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(suspense): don't immediately resolve suspense on last dep unmount (#13456)
authoredison <daiwei521@126.com>
Wed, 20 Aug 2025 14:11:16 +0000 (22:11 +0800)
committerGitHub <noreply@github.com>
Wed, 20 Aug 2025 14:11:16 +0000 (22:11 +0800)
close #13453

packages/runtime-core/__tests__/components/Suspense.spec.ts
packages/runtime-core/src/renderer.ts

index 563c91a179df2cc6f36b811b9e7b6e1ae9dd6cfa..c6683d6a257aa13d9f6516794ee21ede888bc964 100644 (file)
@@ -17,6 +17,8 @@ import {
   onUnmounted,
   ref,
   render,
+  renderList,
+  renderSlot,
   resolveDynamicComponent,
   serializeInner,
   shallowRef,
@@ -2161,6 +2163,80 @@ describe('Suspense', () => {
     await Promise.all(deps)
   })
 
+  // #13453
+  test('add new async deps during patching', async () => {
+    const getComponent = (type: string) => {
+      if (type === 'A') {
+        return defineAsyncComponent({
+          setup() {
+            return () => h('div', 'A')
+          },
+        })
+      }
+      return defineAsyncComponent({
+        setup() {
+          return () => h('div', 'B')
+        },
+      })
+    }
+
+    const types = ref(['A'])
+    const add = async () => {
+      types.value.push('B')
+    }
+
+    const update = async () => {
+      // mount Suspense B
+      // [Suspense A] -> [Suspense A(pending), Suspense B(pending)]
+      await add()
+      // patch Suspense B (still pending)
+      // [Suspense A(pending), Suspense B(pending)] -> [Suspense B(pending)]
+      types.value.shift()
+    }
+
+    const Comp = {
+      render(this: any) {
+        return h(Fragment, null, [
+          renderList(types.value, type => {
+            return h(
+              Suspense,
+              { key: type },
+              {
+                default: () => [
+                  renderSlot(this.$slots, 'default', { type: type }),
+                ],
+              },
+            )
+          }),
+        ])
+      },
+    }
+
+    const App = {
+      setup() {
+        return () =>
+          h(Comp, null, {
+            default: (params: any) => [h(getComponent(params.type))],
+          })
+      },
+    }
+
+    const root = nodeOps.createElement('div')
+    render(h(App), root)
+    expect(serializeInner(root)).toBe(`<!---->`)
+
+    await Promise.all(deps)
+    expect(serializeInner(root)).toBe(`<div>A</div>`)
+
+    update()
+    await nextTick()
+    // wait for both A and B to resolve
+    await Promise.all(deps)
+    // wait for new B to resolve
+    await Promise.all(deps)
+    expect(serializeInner(root)).toBe(`<div>B</div>`)
+  })
+
   describe('warnings', () => {
     // base function to check if a combination of slots warns or not
     function baseCheckWarn(
index 47431a04172d15e3fddc9dcc62b3703a8510c0b8..89b8be6a18047d1080f66128d3afe46e6d2a5e20 100644 (file)
@@ -2326,24 +2326,6 @@ function baseCreateRenderer(
       instance.isUnmounted = true
     }, parentSuspense)
 
-    // A component with async dep inside a pending suspense is unmounted before
-    // its async dep resolves. This should remove the dep from the suspense, and
-    // cause the suspense to resolve immediately if that was the last dep.
-    if (
-      __FEATURE_SUSPENSE__ &&
-      parentSuspense &&
-      parentSuspense.pendingBranch &&
-      !parentSuspense.isUnmounted &&
-      instance.asyncDep &&
-      !instance.asyncResolved &&
-      instance.suspenseId === parentSuspense.pendingId
-    ) {
-      parentSuspense.deps--
-      if (parentSuspense.deps === 0) {
-        parentSuspense.resolve()
-      }
-    }
-
     if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
       devtoolsComponentRemoved(instance)
     }