Repository navigation
Description
Reproduced on next at 2f7dc84 (2026-10-26), with @solidjs/signals built from source. Run: node --conditions=browser --conditions=development dv18-projection-relanding.mjs [row-memo|direct].
Rule. RULES-DERIVED D-30: each landing of a derived store's source reconciles its rows, and their readers show it.
Shape. A createProjection(() => src(), [], { key: "id" }) over an async memo. mapArray runs over the rows with a memo per row that reads row.name, which is what a <For> row's {row.name} compiles to. Two flights land one after the other.
Observed.
row-memo direct
ap lands "apple,apricot" "apple,apricot"
app lands "apple,apricot" ✗ "apple,applesauce,apple pie"
With a memo per row, the rows keep the first landing forever. A render effect that reads row.name directly follows both landings.
Bisected. Fine at f811097 (#4278). Broken at 1212b77 (#4280, "derived store follows a memo's later pending and errors", the fix for #4271) and on 2f7dc84. Only packages/signals/src was swapped between those two builds.
Repro
dv18-projection-relanding.mjs
import { createRoot, createSignal, createMemo, createProjection, createRenderEffect, mapArray, flush } from "@solidjs/signals";
const mode = process.argv[2] ?? "row-memo";
const [q, setQ] = createSignal("");
const res = {}; let screen = "";
createRoot(() => {
const src = createMemo(() => (q() === "" ? [] : new Promise(r => (res[q()] = r))));
const rows = createProjection(() => src(), [], { key: "id" });
if (mode === "direct") createRenderEffect(() => rows.map(r => r.name).join(","), v => { screen = v; });
else {
const views = mapArray(() => rows, row => { const name = createMemo(() => row.name); return () => name(); });
createRenderEffect(() => views().map(f => f()).join(","), v => { screen = v; });
}
});
const tick = async () => { for (let i = 0; i < 3; i++) { await new Promise(r => setTimeout(r, 0)); flush(); } };
const show = l => console.log(l.padEnd(18), JSON.stringify(screen));
flush(); await tick(); show("mount");
setQ("ap"); flush(); await tick(); show("q = ap");
res.ap([{ id: 1, name: "apple" }, { id: 2, name: "apricot" }]); await tick(); show("ap lands");
setQ("app"); flush(); await tick(); show("q = app");
res.app([{ id: 1, name: "apple" }, { id: 3, name: "applesauce" }, { id: 4, name: "apple pie" }]); await tick(); show("app lands"); // expected "apple,applesauce,apple pie"#4271 has two more cases left after #4280. Both are seen in an app with a keyed result list. (1) After the projection's memo has failed, the next re-ask reports no isPending through the projection, although a plain memo does. (2) In one app shape, a re-ask after a landing shows no isPending either. That one is not reduced yet.
Found by ReactiveScript's conformance suite, which diffs its spec against Solid's SCENARIOS.md.
Activity
Record DV-18 as filed: solidjs/solid#4305
fix(signals): keyed derived store over an async memo follows every la…
ryansolid commentedon Oct 27, 2026
Fixed on
nextby #4307 (e509ad1); it ships in the next RC.Generated by Claude Code