Skip to content

Commit 3282281

Browse files
committed
fix: early schedule computed qrls to resolve
1 parent 864ac34 commit 3282281

File tree

6 files changed

+114
-16
lines changed

6 files changed

+114
-16
lines changed

.changeset/calm-hounds-brake.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: early schedule computed qrls to resolve

packages/qwik/src/core/client/dom-container.ts

+30-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import { emitEvent } from '../shared/qrl/qrl-class';
88
import type { QRL } from '../shared/qrl/qrl.public';
99
import { ChoreType } from '../shared/util-chore-type';
1010
import { _SharedContainer } from '../shared/shared-container';
11-
import { inflateQRL, parseQRL, wrapDeserializerProxy } from '../shared/shared-serialization';
11+
import {
12+
TypeIds,
13+
inflateQRL,
14+
parseQRL,
15+
wrapDeserializerProxy,
16+
} from '../shared/shared-serialization';
1217
import { QContainerValue, type HostElement, type ObjToProxyMap } from '../shared/types';
1318
import { EMPTY_ARRAY } from '../shared/utils/flyweight';
1419
import {
@@ -155,11 +160,16 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
155160
element.setAttribute(QContainerAttr, QContainerValue.RESUMED);
156161
element.qContainer = this;
157162

163+
this.$qFuncs$ = getQFuncs(document, this.$instanceHash$) || EMPTY_ARRAY;
164+
this.$setServerData$();
165+
element.setAttribute(QContainerAttr, QContainerValue.RESUMED);
166+
element.qContainer = this;
158167
const qwikStates = element.querySelectorAll('script[type="qwik/state"]');
159168
if (qwikStates.length !== 0) {
160169
const lastState = qwikStates[qwikStates.length - 1];
161170
this.$rawStateData$ = JSON.parse(lastState.textContent!);
162171
this.$stateData$ = wrapDeserializerProxy(this, this.$rawStateData$) as unknown[];
172+
this.$scheduleQRLs$();
163173
}
164174
}
165175

@@ -371,4 +381,23 @@ export class DomContainer extends _SharedContainer implements IClientContainer {
371381
}
372382
this.$serverData$ = { containerAttributes };
373383
}
384+
385+
/**
386+
* Schedule all computed signals to be inflated. This is done after at the time of DomContainer
387+
* creation to ensure that all computed signals are inflated and QRLs are resolved before any
388+
* signals are used. This is necessary because if a computed QRL is not resolved, it will throw a
389+
* promise and we will have to rerun the entire function, which we want to avoid.
390+
*/
391+
private $scheduleQRLs$(): void {
392+
const deserializeValue = <T>(i: number) => {
393+
return this.$stateData$[i / 2] as T;
394+
};
395+
for (let i = 0; i < this.$rawStateData$.length; i += 2) {
396+
const type = this.$rawStateData$[i];
397+
if (type === TypeIds.ComputedSignal) {
398+
// use deserializer proxy to inflate the computed signal and schedule computed QRL
399+
deserializeValue(i);
400+
}
401+
}
402+
}
374403
}

packages/qwik/src/core/shared/utils/markers.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { QContainerValue } from '../types';
2+
import { EMPTY_ARRAY } from './flyweight';
23

34
/** State factory of the component. */
45
export const OnRenderProp = 'q:renderFn';
@@ -23,7 +24,7 @@ export const getQFuncs = (
2324
document: Document,
2425
hash: string
2526
): Array<(...args: unknown[]) => unknown> => {
26-
return (document as any)[QFuncsPrefix + hash] || [];
27+
return (document as any)[QFuncsPrefix + hash] || EMPTY_ARRAY;
2728
};
2829

2930
export const QRenderAttr = 'q:render';

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

+40-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,51 @@
1-
import { $, component$, useComputed$, useSignal } from "@qwik.dev/core";
1+
import {
2+
$,
3+
component$,
4+
useComputed$,
5+
useSignal,
6+
useVisibleTask$,
7+
} from "@qwik.dev/core";
28

39
export const QRL = component$(() => {
10+
const render = useSignal(0);
411
return (
512
<>
6-
<ShouldResolveInnerComputedQRL />
13+
<button id="rerender" onClick$={() => render.value++}>
14+
rerender {render.value}
15+
</button>
16+
<div key={render.value}>
17+
<ShouldResolveComputedQRL />
18+
<ShouldResolveInnerComputedQRL />
19+
</div>
720
</>
821
);
922
});
1023

24+
export const ShouldResolveComputedQRL = component$(() => {
25+
const computedCounter = useSignal(0);
26+
const text = useComputed$(() => {
27+
computedCounter.value++;
28+
return "";
29+
});
30+
31+
return (
32+
<>
33+
<span id="computed-counter">{computedCounter.value}</span>
34+
<Test text={text} />
35+
</>
36+
);
37+
});
38+
39+
export const Test = component$<any>(({ text }) => {
40+
const visibleCounter = useSignal(0);
41+
useVisibleTask$(async () => {
42+
visibleCounter.value++;
43+
text.value;
44+
});
45+
46+
return <div id="visible-counter">{visibleCounter.value}</div>;
47+
});
48+
1149
export const ShouldResolveInnerComputedQRL = component$(() => {
1250
const test = useComputed$(() => 0);
1351
return <InnerComputedButton test={test} />;

starters/apps/e2e/src/root.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ import { TreeshakingApp } from "./components/treeshaking/treeshaking";
3333
import { TwoListeners } from "./components/two-listeners/twolisteners";
3434
import { UseId } from "./components/useid/useid";
3535
import { Watch } from "./components/watch/watch";
36+
import { QRL } from "./components/qrl/qrl";
3637

3738
import "./global.css";
38-
import { QRL } from "./components/qrl/qrl";
3939

4040
const tests: Record<string, FunctionComponent> = {
4141
"/e2e/two-listeners": () => <TwoListeners />,

starters/e2e/e2e.qrl.e2e.ts

+36-11
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,39 @@
1-
import { expect, test } from "@playwright/test";
2-
test("should update counter without uncaught promises", async ({ page }) => {
3-
await page.goto("/e2e/qrl");
4-
page.on("pageerror", (err) => expect(err).toEqual(undefined));
5-
page.on("console", (msg) => {
6-
if (msg.type() === "error") {
7-
expect(msg.text()).toEqual(undefined);
8-
}
1+
import { test, expect } from "@playwright/test";
2+
3+
test.describe("qrl", () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto("/e2e/qrl");
6+
page.on("pageerror", (err) => expect(err).toEqual(undefined));
7+
page.on("console", (msg) => {
8+
if (msg.type() === "error") {
9+
expect(msg.text()).toEqual(undefined);
10+
}
11+
});
912
});
10-
const button = page.locator("#inner-computed-button");
11-
await expect(button).toContainText("Click Me 0");
1213

13-
await button.click();
14+
function tests() {
15+
test("should resolve computed QRL", async ({ page }) => {
16+
const computedCounter = page.locator("#computed-counter");
17+
const visibleCounter = page.locator("#visible-counter");
18+
await expect(computedCounter).toHaveText("1");
19+
await expect(visibleCounter).toHaveText("1");
20+
});
21+
22+
test("should resolve inner computed QRL", async ({ page }) => {
23+
const innerButton = page.locator("#inner-computed-button");
24+
await innerButton.click();
25+
await expect(innerButton).toHaveText("Click Me 1");
26+
});
27+
}
28+
29+
tests();
30+
31+
test.describe("client rerender", () => {
32+
test.beforeEach(async ({ page }) => {
33+
const toggleRender = page.locator("#rerender");
34+
await toggleRender.click();
35+
await expect(page.locator("#rerender")).toHaveText("rerender 1");
36+
});
37+
tests();
38+
});
1439
});

0 commit comments

Comments
 (0)