Skip to content

[2.0 / next] Regression from #4280: a keyed projection's per-row memos miss every landing after the first #4305

Closed

Description

@ryansolid

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

  1. ryansolid commented on Oct 27, 2026

    @ryansolid
    MemberAuthor

    Fixed on next by #4307 (e509ad1); it ships in the next RC.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions