diff --git a/packages/runtime-core/__tests__/rendererSuspense.spec.ts b/packages/runtime-core/__tests__/rendererSuspense.spec.ts
index e73a81c3..7068ab0a 100644
--- a/packages/runtime-core/__tests__/rendererSuspense.spec.ts
+++ b/packages/runtime-core/__tests__/rendererSuspense.spec.ts
@@ -658,7 +658,49 @@ describe('renderer: suspense', () => {
)
})
- test.todo('new async dep after resolve should cause suspense to restart')
+ test('new async dep after resolve should cause suspense to restart', async () => {
+ const toggle = ref(false)
+
+ const ChildA = createAsyncComponent({
+ setup() {
+ return () => h('div', 'Child A')
+ }
+ })
+
+ const ChildB = createAsyncComponent({
+ setup() {
+ return () => h('div', 'Child B')
+ }
+ })
+
+ const Comp = {
+ setup() {
+ return () =>
+ h(Suspense, null, {
+ default: [h(ChildA), toggle.value ? h(ChildB) : null],
+ fallback: h('div', 'root fallback')
+ })
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ render(h(Comp), root)
+ expect(serializeInner(root)).toBe(`
root fallback
`)
+
+ await deps[0]
+ await nextTick()
+ expect(serializeInner(root)).toBe(`Child A
`)
+
+ toggle.value = true
+ await nextTick()
+ expect(serializeInner(root)).toBe(`root fallback
`)
+
+ await deps[1]
+ await nextTick()
+ expect(serializeInner(root)).toBe(
+ `Child A
Child B
`
+ )
+ })
test.todo('portal inside suspense')
})
diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts
index 73488df3..22cf5ef9 100644
--- a/packages/runtime-core/src/createRenderer.ts
+++ b/packages/runtime-core/src/createRenderer.ts
@@ -729,7 +729,9 @@ export function createRenderer<
parentComponent,
container,
hiddenContainer,
- anchor
+ anchor,
+ isSVG,
+ optimized
))
const { content, fallback } = normalizeSuspenseChildren(n2)
@@ -831,20 +833,20 @@ export function createRenderer<
if (__DEV__) {
if (suspense.isResolved) {
throw new Error(
- `suspense.resolve() is called when it's already resolved`
+ `resolveSuspense() is called on an already resolved suspense boundary.`
)
}
if (suspense.isUnmounted) {
throw new Error(
- `suspense.resolve() is called when it's already unmounted`
+ `resolveSuspense() is called on an already unmounted suspense boundary.`
)
}
}
const {
+ vnode,
subTree,
fallbackTree,
effects,
- vnode,
parentComponent,
container
} = suspense
@@ -891,6 +893,47 @@ export function createRenderer<
}
}
+ function restartSuspense(suspense: HostSuspsenseBoundary) {
+ suspense.isResolved = false
+ const {
+ vnode,
+ subTree,
+ fallbackTree,
+ parentComponent,
+ container,
+ hiddenContainer,
+ isSVG,
+ optimized
+ } = suspense
+
+ // move content tree back to the off-dom container
+ const anchor = getNextHostNode(subTree)
+ move(subTree as HostVNode, hiddenContainer, null)
+ // remount the fallback tree
+ patch(
+ null,
+ fallbackTree,
+ container,
+ anchor,
+ parentComponent,
+ null, // fallback tree will not have suspense context
+ isSVG,
+ optimized
+ )
+ const el = (vnode.el = (fallbackTree as HostVNode).el as HostNode)
+ // suspense as the root node of a component...
+ if (parentComponent && parentComponent.subTree === vnode) {
+ parentComponent.vnode.el = el
+ updateHOCHostEl(parentComponent, el)
+ }
+
+ // invoke @suspense event
+ const onSuspense = vnode.props && vnode.props.onSuspense
+ if (isFunction(onSuspense)) {
+ onSuspense()
+ }
+ }
+
function processComponent(
n1: HostVNode | null,
n2: HostVNode,
@@ -986,10 +1029,16 @@ export function createRenderer<
// TODO handle this properly
throw new Error('Async component without a suspense boundary!')
}
+
+ // parent suspense already resolved, need to re-suspense
+ // use queueJob so it's handled synchronously after patching the current
+ // suspense tree
if (parentSuspense.isResolved) {
- // TODO if parentSuspense is already resolved it needs to enter waiting
- // state again
+ queueJob(() => {
+ restartSuspense(parentSuspense)
+ })
}
+
parentSuspense.deps++
instance.asyncDep
.catch(err => {
@@ -1006,6 +1055,7 @@ export function createRenderer<
)
}
})
+
// give it a placeholder
const placeholder = (instance.subTree = createVNode(Empty))
processEmptyNode(null, placeholder, container, anchor)
@@ -1118,8 +1168,7 @@ export function createRenderer<
parentSuspense,
isSVG
)
- let current = instance.vnode
- current.el = nextTree.el
+ instance.vnode.el = nextTree.el
if (next === null) {
// self-triggered update. In case of HOC, update parent component
// vnode el. HOC is indicated by parent instance's subTree pointing
diff --git a/packages/runtime-core/src/suspense.ts b/packages/runtime-core/src/suspense.ts
index fb8c36fb..09a067ab 100644
--- a/packages/runtime-core/src/suspense.ts
+++ b/packages/runtime-core/src/suspense.ts
@@ -13,6 +13,8 @@ export interface SuspenseBoundary<
vnode: HostVNode
parent: SuspenseBoundary | null
parentComponent: ComponentInternalInstance | null
+ isSVG: boolean
+ optimized: boolean
container: HostElement
hiddenContainer: HostElement
anchor: HostNode | null
@@ -30,12 +32,16 @@ export function createSuspenseBoundary(
parentComponent: ComponentInternalInstance | null,
container: HostElement,
hiddenContainer: HostElement,
- anchor: HostNode | null
+ anchor: HostNode | null,
+ isSVG: boolean,
+ optimized: boolean
): SuspenseBoundary {
return {
vnode,
parent,
parentComponent,
+ isSVG,
+ optimized,
container,
hiddenContainer,
anchor,