Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(keep-alive): avoid duplicate mounts of deactivate components #12042

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
44 changes: 44 additions & 0 deletions packages/runtime-core/__tests__/components/KeepAlive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1173,4 +1173,48 @@ describe('KeepAlive', () => {
expect(deactivatedHome).toHaveBeenCalledTimes(0)
expect(unmountedHome).toHaveBeenCalledTimes(1)
})

// #12017
test('avoid duplicate mounts of deactivate components', async () => {
const About = {
name: 'About',
setup() {
return () => h('h1', 'About')
},
}
const mountedHome = vi.fn()
const Home = {
name: 'Home',
setup() {
onMounted(mountedHome)
return () => h('h1', 'Home')
},
}
const activeView = shallowRef(About)
const HomeView = {
name: 'HomeView',
setup() {
return () => h(activeView.value)
},
}

const App = createApp({
setup() {
return () => {
return [
h(KeepAlive, null, [
h(HomeView, {
key: activeView.value.name,
}),
]),
]
}
},
})
App.mount(nodeOps.createElement('div'))
expect(mountedHome).toHaveBeenCalledTimes(0)
activeView.value = Home
await nextTick()
expect(mountedHome).toHaveBeenCalledTimes(1)
})
})
8 changes: 8 additions & 0 deletions packages/runtime-core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,16 @@ export interface ComponentInternalInstance {
*/
asyncResolved: boolean

/**
* effects to be triggered on component activated in keep-alive
* @internal
*/
activatedEffects?: Function[]

// lifecycle
isMounted: boolean
isUnmounted: boolean
isActivated: boolean
isDeactivated: boolean
/**
* @internal
Expand Down Expand Up @@ -673,6 +680,7 @@ export function createComponentInstance(
// not using enums here because it results in computed properties
isMounted: false,
isUnmounted: false,
isActivated: true,
isDeactivated: false,
bc: null,
c: null,
Expand Down
10 changes: 10 additions & 0 deletions packages/runtime-core/src/components/KeepAlive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { devtoolsComponentAdded } from '../devtools'
import { isAsyncWrapper } from '../apiAsyncComponent'
import { isSuspense } from './Suspense'
import { LifecycleHooks } from '../enums'
import { queuePostFlushCb } from '../scheduler'

type MatchPattern = string | RegExp | (string | RegExp)[]

Expand Down Expand Up @@ -136,6 +137,7 @@ const KeepAliveImpl: ComponentOptions = {
optimized,
) => {
const instance = vnode.component!
instance.isActivated = true
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// in case props have changed
patch(
Expand All @@ -149,6 +151,13 @@ const KeepAliveImpl: ComponentOptions = {
vnode.slotScopeIds,
optimized,
)

const effects = instance.activatedEffects
if (effects) {
queuePostFlushCb(effects)
instance.activatedEffects!.length = 0
}

queuePostRenderEffect(() => {
instance.isDeactivated = false
if (instance.a) {
Expand All @@ -168,6 +177,7 @@ const KeepAliveImpl: ComponentOptions = {

sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
instance.isActivated = false
invalidateMount(instance.m)
invalidateMount(instance.a)

Expand Down
28 changes: 28 additions & 0 deletions packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,21 @@ function baseCreateRenderer(
} else {
let { next, bu, u, parent, vnode } = instance

// skip updates while parent component is deactivated
// but store effects for next activation
const deactivatedParent = locateDeactivatedParent(instance)
if (deactivatedParent) {
;(
deactivatedParent.activatedEffects ||
(deactivatedParent.activatedEffects = [])
).push(() => {
if (!instance.isUnmounted) {
update()
}
})
return
}

if (__FEATURE_SUSPENSE__) {
const nonHydratedAsyncRoot = locateNonHydratedAsyncRoot(instance)
// we are trying to update some async comp before hydration
Expand Down Expand Up @@ -2542,6 +2557,19 @@ function locateNonHydratedAsyncRoot(
}
}

function locateDeactivatedParent(instance: ComponentInternalInstance | null) {
while (instance) {
if (!instance.isActivated) {
return instance
}
if (isKeepAlive(instance.vnode)) {
break
}
instance = instance.parent
}
return null
}

export function invalidateMount(hooks: LifecycleHook): void {
if (hooks) {
for (let i = 0; i < hooks.length; i++)
Expand Down