diff --git a/.changeset/heavy-grapes-sin.md b/.changeset/heavy-grapes-sin.md new file mode 100644 index 000000000..516222c31 --- /dev/null +++ b/.changeset/heavy-grapes-sin.md @@ -0,0 +1,5 @@ +--- +"solid-js": patch +--- + +Set committed value for computations created during transition diff --git a/packages/solid/src/reactive/signal.ts b/packages/solid/src/reactive/signal.ts index ddce76160..97caeaa4c 100644 --- a/packages/solid/src/reactive/signal.ts +++ b/packages/solid/src/reactive/signal.ts @@ -1417,6 +1417,8 @@ function runComputation(node: Computation, value: any, time: number) { if (node.updatedAt != null && "observers" in node) { writeSignal(node as Memo, nextValue, true); } else if (Transition && Transition.running && node.pure) { + // On first computation during transition, also set committed value #2046 + if (!Transition.sources.has(node as Memo)) node.value = nextValue; Transition.sources.add(node as Memo); (node as Memo).tValue = nextValue; } else node.value = nextValue; diff --git a/packages/solid/web/test/transition.spec.tsx b/packages/solid/web/test/transition.spec.tsx new file mode 100644 index 000000000..064aa4eaa --- /dev/null +++ b/packages/solid/web/test/transition.spec.tsx @@ -0,0 +1,72 @@ +/** + * @jsxImportSource solid-js + * @vitest-environment jsdom + */ + +import { describe, expect, test } from "vitest"; +import { createSignal, createMemo, createResource, useTransition } from "../../src/index.js"; +import { render, Suspense } from "../src/index.js"; + +describe("Transition memo stale read (#2046)", () => { + test("memo created during transition should not return undefined in committed state", async () => { + const div = document.createElement("div"); + const [route, setRoute] = createSignal("home"); + const [dbVersion, setDbVersion] = createSignal(1); + const [pending, start] = useTransition(); + let dataRef: (() => { q: number }) | null = null; + let resolveResource: (v: string) => void; + + function RouteComponent() { + // Always returns {q:42}. Never undefined. + const data = createMemo(() => ({ q: 42 })); + // Reads both dbVersion (external signal) and data + const label = createMemo(() => dbVersion() + ": " + data()!.q); + dataRef = data; + return

{label()}

; + } + + let fetchCount = 0; + const dispose = render(() => { + const [resource] = createResource( + () => route(), + r => { + fetchCount++; + // First fetch resolves immediately + if (fetchCount <= 1) return Promise.resolve(r); + // Second fetch (during transition) stays pending + return new Promise(resolve => { + resolveResource = resolve; + }); + } + ); + return ( + +

{resource()}

+ {route() === "detail" && } +
+ ); + }, div); + + // Wait for initial resource to resolve + await Promise.resolve(); + await Promise.resolve(); + + // Navigate via transition — resource refetches, keeps transition pending + start(() => setRoute("detail")); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + // RouteComponent mounted during transition, transition is pending + expect(dataRef).not.toBeNull(); + expect(pending()).toBe(true); + + // External signal change while transition is pending. + // label recomputes → reads data() → should be {q:42}, not undefined. + setDbVersion(2); + expect(dataRef!()).toEqual({ q: 42 }); + + resolveResource!("done"); + dispose(); + }); +});