Skip to content

Commit eb493de

Browse files
committed
fix: signal wrapper should not rerender
1 parent 926c518 commit eb493de

File tree

5 files changed

+85
-6
lines changed

5 files changed

+85
-6
lines changed

.changeset/unlucky-olives-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: signal wrapper should not rerender causing missing child error

packages/qwik/src/core/client/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,7 @@ export const enum VNodeFlags {
9696
}
9797

9898
export const enum VNodeFlagsIndex {
99-
mask /* ************* */ = ~0b11_111111,
100-
negated_mask /* ****** */ = 0b11_111111,
99+
mask /* ************** */ = 0b11_111111,
101100
shift /* ************* */ = 8,
102101
}
103102

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -991,7 +991,13 @@ export const vnode_diff = (
991991
}
992992

993993
function expectVirtual(type: VirtualType, jsxKey: string | null) {
994-
if (vCurrent && vnode_isVirtualVNode(vCurrent) && getKey(vCurrent) === jsxKey && !!jsxKey) {
994+
const checkKey = type === VirtualType.Fragment;
995+
if (
996+
vCurrent &&
997+
vnode_isVirtualVNode(vCurrent) &&
998+
getKey(vCurrent) === jsxKey &&
999+
(checkKey ? !!jsxKey : true)
1000+
) {
9951001
// All is good.
9961002
return;
9971003
} else if (jsxKey !== null) {

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

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Fragment, _jsxSorted } from '@qwik.dev/core';
1+
import { Fragment, _fnSignal, _jsxSorted } from '@qwik.dev/core';
22
import { vnode_fromJSX } from '@qwik.dev/core/testing';
33
import { describe, expect, it } from 'vitest';
44
import { vnode_applyJournal, vnode_getFirstChild, vnode_getNode } from './vnode';
55
import { vnode_diff } from './vnode-diff';
66
import type { QElement } from '../shared/types';
77
import { createSignal } from '../reactive-primitives/signal-api';
88
import { QError, qError } from '../shared/error/error';
9+
import type { VirtualVNode } from './types';
910

1011
describe('vNode-diff', () => {
1112
it('should find no difference', () => {
@@ -430,7 +431,75 @@ describe('vNode-diff', () => {
430431
expect(b2).toBe(selectB2());
431432
});
432433
});
433-
describe.todo('fragments', () => {});
434+
describe('fragments', () => {
435+
it('should not rerender signal wrapper fragment', async () => {
436+
const { vNode, vParent, container } = vnode_fromJSX(
437+
_jsxSorted('div', {}, null, [_jsxSorted(Fragment, {}, null, ['1'], 0, null)], 0, null)
438+
);
439+
const test = _jsxSorted('div', {}, null, [_fnSignal(() => '2', [], '() => "2"')], 0, null);
440+
const expected = _jsxSorted(
441+
'div',
442+
{},
443+
null,
444+
[_jsxSorted(Fragment, {}, null, ['2'], 0, null)],
445+
0,
446+
null
447+
);
448+
449+
const signalFragment = vnode_getFirstChild(vNode!) as VirtualVNode;
450+
expect(signalFragment).toBeDefined();
451+
452+
vnode_diff(container, test, vParent, null);
453+
vnode_applyJournal(container.$journal$);
454+
expect(vNode).toMatchVDOM(expected);
455+
expect(signalFragment).toBe(vnode_getFirstChild(vNode!));
456+
});
457+
458+
it('should not rerender promise wrapper fragment', async () => {
459+
const { vNode, vParent, container } = vnode_fromJSX(
460+
_jsxSorted('div', {}, null, [_jsxSorted(Fragment, {}, null, ['1'], 0, null)], 0, null)
461+
);
462+
const test = _jsxSorted('div', {}, null, [Promise.resolve('2')], 0, null);
463+
const expected = _jsxSorted(
464+
'div',
465+
{},
466+
null,
467+
[_jsxSorted(Fragment, {}, null, ['2'], 0, null)],
468+
0,
469+
null
470+
);
471+
472+
const promiseFragment = vnode_getFirstChild(vNode!) as VirtualVNode;
473+
expect(promiseFragment).toBeDefined();
474+
475+
await vnode_diff(container, test, vParent, null);
476+
vnode_applyJournal(container.$journal$);
477+
expect(vNode).toMatchVDOM(expected);
478+
expect(promiseFragment).toBe(vnode_getFirstChild(vNode!));
479+
});
480+
481+
it('should rerender fragment if no key', async () => {
482+
const { vNode, vParent, container } = vnode_fromJSX(
483+
_jsxSorted('div', {}, null, [_jsxSorted(Fragment, {}, null, ['1'], 0, null)], 0, null)
484+
);
485+
const test = _jsxSorted(
486+
'div',
487+
{},
488+
null,
489+
[_jsxSorted(Fragment, {}, null, ['2'], 0, null)],
490+
0,
491+
null
492+
);
493+
494+
const fragment = vnode_getFirstChild(vNode!) as VirtualVNode;
495+
expect(fragment).toBeDefined();
496+
497+
await vnode_diff(container, test, vParent, null);
498+
vnode_applyJournal(container.$journal$);
499+
expect(vNode).toMatchVDOM(test);
500+
expect(fragment).not.toBe(vnode_getFirstChild(vNode!));
501+
});
502+
});
434503
describe('attributes', () => {
435504
describe('const props', () => {
436505
it('should set attributes', async () => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1810,7 +1810,7 @@ function materializeFromVNodeData(
18101810

18111811
const addVNode = (node: VNode) => {
18121812
node[VNodeProps.flags] =
1813-
(node[VNodeProps.flags] & VNodeFlagsIndex.negated_mask) | (idx << VNodeFlagsIndex.shift);
1813+
(node[VNodeProps.flags] & VNodeFlagsIndex.mask) | (idx << VNodeFlagsIndex.shift);
18141814
idx++;
18151815
vLast && (vLast[VNodeProps.nextSibling] = node);
18161816
node[VNodeProps.previousSibling] = vLast;

0 commit comments

Comments
 (0)