Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/heavy-grapes-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"solid-js": patch
---

Set committed value for computations created during transition
2 changes: 2 additions & 0 deletions packages/solid/src/reactive/signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1417,6 +1417,8 @@ function runComputation(node: Computation<any>, value: any, time: number) {
if (node.updatedAt != null && "observers" in node) {
writeSignal(node as Memo<any>, 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<any>)) node.value = nextValue;
Transition.sources.add(node as Memo<any>);
(node as Memo<any>).tValue = nextValue;
} else node.value = nextValue;
Expand Down
72 changes: 72 additions & 0 deletions packages/solid/web/test/transition.spec.tsx
Original file line number Diff line number Diff line change
@@ -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 <p>{label()}</p>;
}

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<string>(resolve => {
resolveResource = resolve;
});
}
);
return (
<Suspense fallback="loading">
<p>{resource()}</p>
{route() === "detail" && <RouteComponent />}
</Suspense>
);
}, 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();
});
});
Loading