Skip to content

Commit 51cce75

Browse files
jpdutoitryansolid
andauthored
Set committed value for computations created during transition (#2617)
* Set committed value for computations created during transition Fixes #2046 * Add changeset for solid-js patch Set committed value for computations created during transition --------- Co-authored-by: Ryan Carniato <ryansolid@gmail.com>
1 parent c58983d commit 51cce75

3 files changed

Lines changed: 79 additions & 0 deletions

File tree

.changeset/heavy-grapes-sin.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"solid-js": patch
3+
---
4+
5+
Set committed value for computations created during transition

packages/solid/src/reactive/signal.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,8 @@ function runComputation(node: Computation<any>, value: any, time: number) {
14171417
if (node.updatedAt != null && "observers" in node) {
14181418
writeSignal(node as Memo<any>, nextValue, true);
14191419
} else if (Transition && Transition.running && node.pure) {
1420+
// On first computation during transition, also set committed value #2046
1421+
if (!Transition.sources.has(node as Memo<any>)) node.value = nextValue;
14201422
Transition.sources.add(node as Memo<any>);
14211423
(node as Memo<any>).tValue = nextValue;
14221424
} else node.value = nextValue;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* @jsxImportSource solid-js
3+
* @vitest-environment jsdom
4+
*/
5+
6+
import { describe, expect, test } from "vitest";
7+
import { createSignal, createMemo, createResource, useTransition } from "../../src/index.js";
8+
import { render, Suspense } from "../src/index.js";
9+
10+
describe("Transition memo stale read (#2046)", () => {
11+
test("memo created during transition should not return undefined in committed state", async () => {
12+
const div = document.createElement("div");
13+
const [route, setRoute] = createSignal("home");
14+
const [dbVersion, setDbVersion] = createSignal(1);
15+
const [pending, start] = useTransition();
16+
let dataRef: (() => { q: number }) | null = null;
17+
let resolveResource: (v: string) => void;
18+
19+
function RouteComponent() {
20+
// Always returns {q:42}. Never undefined.
21+
const data = createMemo(() => ({ q: 42 }));
22+
// Reads both dbVersion (external signal) and data
23+
const label = createMemo(() => dbVersion() + ": " + data()!.q);
24+
dataRef = data;
25+
return <p>{label()}</p>;
26+
}
27+
28+
let fetchCount = 0;
29+
const dispose = render(() => {
30+
const [resource] = createResource(
31+
() => route(),
32+
r => {
33+
fetchCount++;
34+
// First fetch resolves immediately
35+
if (fetchCount <= 1) return Promise.resolve(r);
36+
// Second fetch (during transition) stays pending
37+
return new Promise<string>(resolve => {
38+
resolveResource = resolve;
39+
});
40+
}
41+
);
42+
return (
43+
<Suspense fallback="loading">
44+
<p>{resource()}</p>
45+
{route() === "detail" && <RouteComponent />}
46+
</Suspense>
47+
);
48+
}, div);
49+
50+
// Wait for initial resource to resolve
51+
await Promise.resolve();
52+
await Promise.resolve();
53+
54+
// Navigate via transition — resource refetches, keeps transition pending
55+
start(() => setRoute("detail"));
56+
await Promise.resolve();
57+
await Promise.resolve();
58+
await Promise.resolve();
59+
60+
// RouteComponent mounted during transition, transition is pending
61+
expect(dataRef).not.toBeNull();
62+
expect(pending()).toBe(true);
63+
64+
// External signal change while transition is pending.
65+
// label recomputes → reads data() → should be {q:42}, not undefined.
66+
setDbVersion(2);
67+
expect(dataRef!()).toEqual({ q: 42 });
68+
69+
resolveResource!("done");
70+
dispose();
71+
});
72+
});

0 commit comments

Comments
 (0)