Skip to content

Commit cc8a4cc

Browse files
committed
fix: cleanup store and signals during vnode remove
1 parent 9a00f77 commit cc8a4cc

File tree

3 files changed

+118
-10
lines changed

3 files changed

+118
-10
lines changed

packages/qwik/src/core/client/vnode-diff.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1547,13 +1547,10 @@ export function cleanup(container: ClientContainer, vNode: VNode) {
15471547
const obj = seq[i];
15481548
if (isObject(obj)) {
15491549
const objIsTask = isTask(obj);
1550-
if (objIsTask) {
1551-
clearAllEffects(container, obj);
1552-
if (obj.$flags$ & TaskFlags.VISIBLE_TASK) {
1553-
container.$scheduler$(ChoreType.CLEANUP_VISIBLE, obj);
1554-
// don't call cleanupDestroyable yet, do it by the scheduler
1555-
continue;
1556-
}
1550+
if (objIsTask && obj.$flags$ & TaskFlags.VISIBLE_TASK) {
1551+
container.$scheduler$(ChoreType.CLEANUP_VISIBLE, obj);
1552+
// don't call cleanupDestroyable yet, do it by the scheduler
1553+
continue;
15571554
} else if (obj instanceof SignalImpl || isStore(obj)) {
15581555
clearAllEffects(container, obj as Consumer);
15591556
}

packages/qwik/src/core/client/vnode-diff.unit.tsx

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
1-
import { Fragment, _fnSignal, _jsxSorted, component$, type JSXOutput } from '@qwik.dev/core';
1+
import {
2+
Fragment,
3+
_fnSignal,
4+
_jsxSorted,
5+
component$,
6+
useSignal,
7+
useStore,
8+
type JSXChildren,
9+
type JSXOutput,
10+
} from '@qwik.dev/core';
211
import { vnode_fromJSX } from '@qwik.dev/core/testing';
312
import { describe, expect, it } from 'vitest';
413
import type { SignalImpl } from '../reactive-primitives/impl/signal-impl';
5-
import { _hasStoreEffects, getOrCreateStore } from '../reactive-primitives/impl/store';
14+
import {
15+
_hasStoreEffects,
16+
getOrCreateStore,
17+
getStoreHandler,
18+
} from '../reactive-primitives/impl/store';
619
import type { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl';
720
import { createSignal } from '../reactive-primitives/signal-api';
821
import { StoreFlags } from '../reactive-primitives/types';
@@ -1581,4 +1594,99 @@ describe('vNode-diff', () => {
15811594
expect((vnode_getNode(vNode) as Element).outerHTML).toEqual('<div q:key="KA_0">Hello</div>');
15821595
});
15831596
});
1597+
1598+
describe('cleanup - clearAllEffects', () => {
1599+
it('should call clearAllEffects for VNode elements', () => {
1600+
const { vParent, container } = vnode_fromJSX(<div key="KA_0">Hello</div>);
1601+
1602+
const signal = createSignal('test') as SignalImpl;
1603+
const test = _jsxSorted('div', { class: signal }, null, [], 0, 'KA_0');
1604+
vnode_diff(container, test, vParent, null);
1605+
vnode_applyJournal(container.$journal$);
1606+
1607+
expect(signal.$effects$).toBeDefined();
1608+
expect(signal.$effects$!.size).toBeGreaterThan(0);
1609+
1610+
vnode_diff(container, _jsxSorted('div', {}, null, [], 0, 'KA_0'), vParent, null);
1611+
vnode_applyJournal(container.$journal$);
1612+
1613+
expect(signal.$effects$!.size).toBe(0);
1614+
});
1615+
1616+
it('should call clearAllEffects for VirtualVNode components', async () => {
1617+
const { vParent, container } = vnode_fromJSX(<Fragment></Fragment>);
1618+
1619+
const signal = createSignal('initial') as SignalImpl;
1620+
1621+
const Child = component$((props: any) => {
1622+
return <span>{props.value}</span>;
1623+
});
1624+
1625+
const test1 = _jsxSorted(Child, null, { value: signal }, null, 3, null) as JSXChildren;
1626+
await vnode_diff(container, test1, vParent, null);
1627+
await waitForDrain(container.$scheduler$);
1628+
vnode_applyJournal(container.$journal$);
1629+
1630+
expect(signal.$effects$).toBeDefined();
1631+
expect(signal.$effects$!.size).toBeGreaterThan(0);
1632+
1633+
await vnode_diff(container, <Fragment></Fragment>, vParent, null);
1634+
await waitForDrain(container.$scheduler$);
1635+
vnode_applyJournal(container.$journal$);
1636+
1637+
expect(signal.$effects$!.size).toBe(0);
1638+
});
1639+
1640+
it('should call clearAllEffects for SignalImpl objects in ELEMENT_SEQ', async () => {
1641+
const { vParent, container } = vnode_fromJSX(<Fragment></Fragment>);
1642+
1643+
(globalThis as any).innerSignal = undefined;
1644+
1645+
const Child = component$(() => {
1646+
(globalThis as any).innerSignal = useSignal('inner');
1647+
return <span>{(globalThis as any).innerSignal.value}</span>;
1648+
});
1649+
1650+
const test = _jsxSorted(Child as unknown as any, null, null, null, 3, null) as any;
1651+
vnode_diff(container, test, vParent, null);
1652+
await waitForDrain(container.$scheduler$);
1653+
vnode_applyJournal(container.$journal$);
1654+
1655+
expect((globalThis as any).innerSignal.$effects$).toBeDefined();
1656+
expect((globalThis as any).innerSignal.$effects$!.size).toBeGreaterThan(0);
1657+
1658+
vnode_diff(container, <Fragment></Fragment>, vParent, null);
1659+
await waitForDrain(container.$scheduler$);
1660+
vnode_applyJournal(container.$journal$);
1661+
1662+
expect((globalThis as any).innerSignal.$effects$.size).toBe(0);
1663+
});
1664+
1665+
it('should call clearAllEffects for Store objects in ELEMENT_SEQ', async () => {
1666+
const { vParent, container } = vnode_fromJSX(<Fragment></Fragment>);
1667+
1668+
(globalThis as any).store = undefined;
1669+
1670+
const Child = component$(() => {
1671+
(globalThis as any).store = useStore({ count: 10 });
1672+
return <span>Count: {(globalThis as any).store.count}</span>;
1673+
});
1674+
1675+
const test = _jsxSorted(Child as unknown as any, null, null, null, 3, null) as any;
1676+
vnode_diff(container, test, vParent, null);
1677+
await waitForDrain(container.$scheduler$);
1678+
vnode_applyJournal(container.$journal$);
1679+
1680+
const store = getStoreHandler((globalThis as any).store);
1681+
1682+
expect(store!.$effects$?.size).toBeGreaterThan(0);
1683+
1684+
container.$journal$ = [];
1685+
vnode_diff(container, <Fragment></Fragment>, vParent, null);
1686+
await waitForDrain(container.$scheduler$);
1687+
vnode_applyJournal(container.$journal$);
1688+
1689+
expect(store!.$effects$?.size).toBe(0);
1690+
});
1691+
});
15841692
});

packages/qwik/src/core/reactive-primitives/cleanup.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,12 @@ function clearAsyncComputedSignal(
7777
function clearStore(producer: StoreHandler, effect: EffectSubscription) {
7878
const effects = producer?.$effects$;
7979
if (effects) {
80-
for (const propEffects of effects.values()) {
80+
for (const [propKey, propEffects] of effects.entries()) {
8181
if (propEffects.has(effect)) {
8282
propEffects.delete(effect);
83+
if (propEffects.size === 0) {
84+
effects.delete(propKey);
85+
}
8386
}
8487
}
8588
}

0 commit comments

Comments
 (0)