Skip to content

Commit cb4934b

Browse files
authored
Merge pull request #7456 from QwikDev/v2-null-key-component
fix: reexecute component with null key
2 parents f3ae77d + fa31110 commit cb4934b

File tree

5 files changed

+115
-5
lines changed

5 files changed

+115
-5
lines changed

.changeset/dirty-lemons-shop.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: reexecute component with null key

.changeset/heavy-kids-wave.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: rendering markdown file with Qwik component

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -984,7 +984,7 @@ export const vnode_diff = (
984984
}
985985

986986
function expectVirtual(type: VirtualType, jsxKey: string | null) {
987-
if (vCurrent && vnode_isVirtualVNode(vCurrent) && getKey(vCurrent) === jsxKey) {
987+
if (vCurrent && vnode_isVirtualVNode(vCurrent) && getKey(vCurrent) === jsxKey && !!jsxKey) {
988988
// All is good.
989989
return;
990990
} else if (jsxKey !== null) {
@@ -1043,7 +1043,7 @@ export const vnode_diff = (
10431043
}
10441044
host = vNewNode as VirtualVNode;
10451045
shouldRender = true;
1046-
} else if (!hashesAreEqual) {
1046+
} else if (!hashesAreEqual || !jsxNode.key) {
10471047
insertNewComponent(host, componentQRL, jsxProps);
10481048
host = vNewNode as VirtualVNode;
10491049
shouldRender = true;

packages/qwik/src/core/tests/component.spec.tsx

+102-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Fragment as Signal,
99
SkipRender,
1010
Slot,
11+
_jsxSorted,
1112
component$,
1213
h,
1314
jsx,
@@ -28,7 +29,8 @@ import { QError } from '../shared/error/error';
2829
import { ErrorProvider } from '../../testing/rendering.unit-util';
2930
import * as qError from '../shared/error/error';
3031
import { QContainerValue } from '../shared/types';
31-
import { QContainerAttr } from '../shared/utils/markers';
32+
import { OnRenderProp, QContainerAttr } from '../shared/utils/markers';
33+
import { vnode_getParent, vnode_getProp, vnode_locate } from '../client/vnode';
3234

3335
const debug = false; //true;
3436
Error.stackTraceLimit = 100;
@@ -1991,6 +1993,105 @@ describe.each([
19911993
);
19921994
});
19931995

1996+
it('should reexecute entire component without key', async () => {
1997+
const Child = component$((props: { text: string }) => {
1998+
const text = useSignal('');
1999+
useTask$(() => {
2000+
text.value = props.text;
2001+
});
2002+
return <div>{text.value}</div>;
2003+
});
2004+
2005+
const Cmp = component$(() => {
2006+
const toggle = useSignal(true);
2007+
2008+
return (
2009+
<>
2010+
<button onClick$={() => (toggle.value = !toggle.value)}></button>
2011+
{/* no key for both components */}
2012+
{toggle.value ? jsx(Child, { text: 'Hello' }, null) : jsx(Child, { text: 'World' }, null)}
2013+
</>
2014+
);
2015+
});
2016+
2017+
const { vNode, document } = await render(<Cmp />, { debug });
2018+
expect(vNode).toMatchVDOM(
2019+
<Component ssr-required>
2020+
<Fragment ssr-required>
2021+
<button></button>
2022+
<Component ssr-required>
2023+
<div>
2024+
<Signal ssr-required>Hello</Signal>
2025+
</div>
2026+
</Component>
2027+
</Fragment>
2028+
</Component>
2029+
);
2030+
await trigger(document.body, 'button', 'click');
2031+
expect(vNode).toMatchVDOM(
2032+
<Component ssr-required>
2033+
<Fragment ssr-required>
2034+
<button></button>
2035+
<Component ssr-required>
2036+
<div>
2037+
<Signal ssr-required>World</Signal>
2038+
</div>
2039+
</Component>
2040+
</Fragment>
2041+
</Component>
2042+
);
2043+
});
2044+
2045+
it('should remove component with null key when it is compared with fragment with null key', async () => {
2046+
const InnerCmp = component$(() => {
2047+
return <div>InnerCmp</div>;
2048+
});
2049+
2050+
const Cmp = component$(() => {
2051+
const toggle = useSignal(true);
2052+
2053+
return (
2054+
<>
2055+
<button onClick$={() => (toggle.value = !toggle.value)}></button>
2056+
{toggle.value ? (
2057+
<InnerCmp key={null} />
2058+
) : (
2059+
<Fragment key={null}>
2060+
<h1>Test</h1>
2061+
</Fragment>
2062+
)}
2063+
</>
2064+
);
2065+
});
2066+
2067+
const { vNode, document, container } = await render(<Cmp />, { debug });
2068+
expect(vNode).toMatchVDOM(
2069+
<Component ssr-required>
2070+
<Fragment ssr-required>
2071+
<button></button>
2072+
<Component ssr-required>
2073+
<div>InnerCmp</div>
2074+
</Component>
2075+
</Fragment>
2076+
</Component>
2077+
);
2078+
await trigger(document.body, 'button', 'click');
2079+
2080+
expect(vNode).toMatchVDOM(
2081+
<Component ssr-required>
2082+
<Fragment ssr-required>
2083+
<button></button>
2084+
<Fragment ssr-required>
2085+
<h1>Test</h1>
2086+
</Fragment>
2087+
</Fragment>
2088+
</Component>
2089+
);
2090+
const h1Element = vnode_locate(container.rootVNode, document.querySelector('h1')!);
2091+
2092+
expect(vnode_getProp(vnode_getParent(h1Element)!, OnRenderProp, null)).toBeNull();
2093+
});
2094+
19942095
describe('regression', () => {
19952096
it('#3643', async () => {
19962097
const Issue3643 = component$(() => {

packages/qwik/src/core/tests/use-on.spec.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ import {
1414
useTask$,
1515
useVisibleTask$,
1616
} from '@qwik.dev/core';
17-
import { domRender, ssrRenderToDom } from '@qwik.dev/core/testing';
17+
import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing';
1818
import { describe, expect, it } from 'vitest';
19-
import { trigger } from '../../testing/element-fixture';
2019

2120
const debug = false; //true;
2221
Error.stackTraceLimit = 100;

0 commit comments

Comments
 (0)