Skip to content

Commit ebd99c6

Browse files
committed
fix: set const and var props directly, compare qrl hashes
1 parent 1440753 commit ebd99c6

File tree

5 files changed

+105
-9
lines changed

5 files changed

+105
-9
lines changed

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
directGetPropsProxyProp,
1010
isJSXNode,
1111
type Props,
12+
type PropsProxy,
1213
} from '../shared/jsx/jsx-runtime';
1314
import { Slot } from '../shared/jsx/slot.public';
1415
import type { JSXNodeInternal, JSXOutput } from '../shared/jsx/types/jsx-node';
@@ -103,6 +104,7 @@ import { getFileLocationFromJsx } from '../shared/utils/jsx-filename';
103104
import { EffectProperty } from '../reactive-primitives/types';
104105
import { SubscriptionData } from '../reactive-primitives/subscription-data';
105106
import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl';
107+
import { _CONST_PROPS, _VAR_PROPS } from '../internal';
106108

107109
export const vnode_diff = (
108110
container: ClientContainer,
@@ -1063,29 +1065,36 @@ export const vnode_diff = (
10631065
}
10641066

10651067
if (host) {
1066-
const vNodeProps = vnode_getProp<any>(host, ELEMENT_PROPS, container.$getObjectById$);
1068+
let vNodeProps = vnode_getProp<any>(host, ELEMENT_PROPS, container.$getObjectById$);
10671069
const propsAreDifferent = propsDiffer(jsxProps, vNodeProps);
10681070
shouldRender = shouldRender || propsAreDifferent;
10691071
if (shouldRender) {
10701072
if (propsAreDifferent) {
10711073
if (vNodeProps) {
10721074
// Reuse the same props instance, qrls can use the current props instance
10731075
// as a capture ref, so we can't change it.
1074-
Object.assign(vNodeProps, jsxProps);
1076+
// We need to do this directly, because normally we would subscribe to the signals
1077+
// if any signal is there.
1078+
vNodeProps[_CONST_PROPS] = (jsxProps as PropsProxy)[_CONST_PROPS];
1079+
vNodeProps[_VAR_PROPS] = (jsxProps as PropsProxy)[_VAR_PROPS];
10751080
} else if (jsxProps) {
1076-
// if there is no props instance, create a new one.
1081+
// If there is no props instance, create a new one.
10771082
// We can do this because we are not using the props instance for anything else.
1078-
container.setHostProp(host, ELEMENT_PROPS, jsxProps);
1083+
vnode_setProp(host, ELEMENT_PROPS, jsxProps);
1084+
vNodeProps = jsxProps;
10791085
}
10801086
}
1087+
// Assign the new QRL instance to the host.
1088+
// Unfortunately it is created every time, something to fix in the optimizer.
1089+
vnode_setProp(host, OnRenderProp, componentQRL);
10811090

10821091
/**
10831092
* Mark host as not deleted. The host could have been marked as deleted if it there was a
10841093
* cleanup run. Now we found it and want to reuse it, so we need to mark it as not
10851094
* deleted.
10861095
*/
10871096
host[VNodeProps.flags] &= ~VNodeFlags.Deleted;
1088-
container.$scheduler$(ChoreType.COMPONENT, host, componentQRL, jsxProps);
1097+
container.$scheduler$(ChoreType.COMPONENT, host, componentQRL, vNodeProps);
10891098
}
10901099
}
10911100
descendContentToProject(jsxNode.children, host);

packages/qwik/src/core/shared/scheduler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ import { ComputedSignalImpl } from '../reactive-primitives/impl/computed-signal-
129129
import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl';
130130
import type { StoreHandler } from '../reactive-primitives/impl/store';
131131
import { SignalImpl } from '../reactive-primitives/impl/signal-impl';
132+
import { isQrl } from './qrl/qrl-utils';
132133

133134
// Turn this on to get debug output of what the scheduler is doing.
134135
const DEBUG: boolean = false;
@@ -588,6 +589,9 @@ export const createScheduler = (
588589

589590
// If the host is the same (or missing), and the type is the same, we need to compare the target.
590591
if (a.$target$ !== b.$target$) {
592+
if (isQrl(a.$target$) && isQrl(b.$target$) && a.$target$.$hash$ === b.$target$.$hash$) {
593+
return 0;
594+
}
591595
// 1 means that we are going to process chores as FIFO
592596
return 1;
593597
}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2305,6 +2305,32 @@ describe.each([
23052305
(globalThis as any).logs = undefined;
23062306
});
23072307

2308+
it('should change component props to new one for the same component with the same key', async () => {
2309+
(globalThis as any).logs = [];
2310+
const FirstCmp = component$((props: { foo?: string; bar?: string }) => {
2311+
(globalThis as any).logs.push('foo' in props, 'bar' in props);
2312+
return <div>{props.foo}</div>;
2313+
});
2314+
2315+
const Cmp = component$(() => {
2316+
const toggle = useSignal(true);
2317+
return (
2318+
<>
2319+
<button onClick$={() => (toggle.value = !toggle.value)}></button>
2320+
{toggle.value ? <FirstCmp key="1" foo="foo" /> : <FirstCmp key="1" bar="bar" />}
2321+
</>
2322+
);
2323+
});
2324+
2325+
const { document } = await render(<Cmp />, { debug });
2326+
expect((globalThis as any).logs).toEqual([true, false]);
2327+
await trigger(document.body, 'button', 'click');
2328+
expect((globalThis as any).logs).toEqual([true, false, false, true]);
2329+
await trigger(document.body, 'button', 'click');
2330+
expect((globalThis as any).logs).toEqual([true, false, false, true, true, false]);
2331+
(globalThis as any).logs = undefined;
2332+
});
2333+
23082334
describe('regression', () => {
23092335
it('#3643', async () => {
23102336
const Issue3643 = component$(() => {

starters/apps/e2e/src/components/render/render.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
type PropsOf,
1414
type QRL,
1515
} from "@qwik.dev/core";
16-
import { h, SSRComment, SSRRaw } from "@qwik.dev/core/internal";
16+
import { h, SSRComment, SSRRaw, type Signal } from "@qwik.dev/core/internal";
1717
import { delay } from "../streaming/demo";
1818

1919
export const Render = component$(() => {
@@ -103,6 +103,7 @@ export const RenderChildren = component$<{ v: number }>(({ v }) => {
103103
<Issue4455 />
104104
<Issue5266 />
105105
<DynamicButton id="dynamic-button" />;
106+
<RerenderOnce />
106107
</>
107108
);
108109
});
@@ -960,3 +961,32 @@ export const DynamicButton = component$<any>(
960961
);
961962
},
962963
);
964+
965+
const globalObj = ["foo", "bar"];
966+
967+
let logs: any[] = [];
968+
969+
const RerenderOnceChild = component$<{ obj: string; foo: Signal<number> }>(
970+
({ obj, foo }) => {
971+
logs.push("render Cmp", obj, foo.value);
972+
return <span id="rerender-once-child">{JSON.stringify(logs)}</span>;
973+
},
974+
);
975+
976+
export const RerenderOnce = component$(() => {
977+
const foo = useSignal(0);
978+
logs = [];
979+
return (
980+
<div>
981+
<button
982+
id="rerender-once-button"
983+
onClick$={() => {
984+
foo.value === 0 ? (foo.value = 1) : (foo.value = 0);
985+
}}
986+
>
987+
click
988+
</button>
989+
<RerenderOnceChild obj={globalObj[foo.value]} foo={foo} />
990+
</div>
991+
);
992+
});

starters/e2e/e2e.render.e2e.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ test.describe("render", () => {
1212
});
1313
});
1414

15-
function tests() {
15+
function tests(isClient: boolean) {
1616
test("should load", async ({ page }) => {
1717
const button = page.locator("button#increment");
1818
const text = page.locator("#rerenders");
@@ -486,9 +486,36 @@ test.describe("render", () => {
486486
await button.click();
487487
await expect(tag).toHaveAttribute("data-v", "bar");
488488
});
489+
490+
test("should rerender child once", async ({ page }) => {
491+
const button = page.locator("#rerender-once-button");
492+
expect(await page.locator("#rerender-once-child").innerHTML()).toEqual(
493+
'["render Cmp","foo",0]',
494+
);
495+
await button.click();
496+
if (isClient) {
497+
await expect(page.locator("#rerender-once-child")).toHaveText(
498+
'["render Cmp","foo",0,"render Cmp","bar",1]',
499+
);
500+
} else {
501+
await expect(page.locator("#rerender-once-child")).toHaveText(
502+
'["render Cmp","bar",1]',
503+
);
504+
}
505+
await button.click();
506+
if (isClient) {
507+
await expect(page.locator("#rerender-once-child")).toHaveText(
508+
'["render Cmp","foo",0,"render Cmp","bar",1,"render Cmp","foo",0]',
509+
);
510+
} else {
511+
await expect(page.locator("#rerender-once-child")).toHaveText(
512+
'["render Cmp","bar",1,"render Cmp","foo",0]',
513+
);
514+
}
515+
});
489516
}
490517

491-
tests();
518+
tests(false);
492519

493520
test.describe("client rerender", () => {
494521
test.beforeEach(async ({ page }) => {
@@ -499,7 +526,7 @@ test.describe("render", () => {
499526
`Render ${Number(v) + 1}`,
500527
);
501528
});
502-
tests();
529+
tests(true);
503530
});
504531

505532
test("pr3475", async ({ page }) => {

0 commit comments

Comments
 (0)