Skip to content

[2.0 / next] A derived store's per-key isPending reads false under a Loading that has shown content (D-30, S-37) #4269

Closed

Description

@ryansolid

Reproduced on next at 2a0da2f (2026-10-24), @solidjs/signals built from source. Run: node --conditions=browser --conditions=development dv4-signals.mjs [store|memo] [boundary|none].

Rule. RULES-DERIVED D-30: "a derived store's leaves report the
derive's new-question refetch like any async memo"
; SCENARIOS S-37 row 2:
after setId(2), isPending(() => state.a) is true.

Shape. createStore(async draft => { Object.assign(draft, await fetch(id())) }, {}),
a render effect showing state.a and isPending(() => state.a), under a
createLoadingBoundary that has shown content. setId(2).

Observed.

                      store, boundary   memo, boundary   store, no boundary
setId(2)              "1 -"   ✗         "1 pending"  ✓   "1 pending"  ✓

The async memo under the same boundary reports pending; the same store
outside a boundary reports pending. Only the store-under-boundary
combination reads false. It shows when the verdict is read in the same computation as the value; with the verdict in its own {…} hole (as compiled JSX reads it) it reads true.

Repro

dv4-signals.mjs

// a derived store's per-key verdicts under a boundary that has shown content.
// Expected (RULES-DERIVED D-30, SCENARIOS S-37 row 2): after setId(2), while the derive re-asks, isPending(() => state.a) is true.
import { createRoot, createSignal, createMemo, createStore, createRenderEffect, createLoadingBoundary, isPending, flush, untrack } from "@solidjs/signals";

const flights = new Map();
const fetchKey = key => { if (!flights.has(key)) { let resolve; const p = new Promise(r => (resolve = r)); flights.set(key, { p, resolve }); } return flights.get(key).p; };
const tick = async () => { await new Promise(r => setTimeout(r, 0)); flush(); await new Promise(r => setTimeout(r, 0)); flush(); };
const land = async (key, v) => { flights.get(key).resolve(v); await tick(); };

const [kind, where] = [process.argv[2] ?? "store", process.argv[3] ?? "boundary"];
let screen = "";
const [id, setId] = createSignal(1);
createRoot(() => {
  let a;
  if (kind === "store") { const [state] = createStore(async draft => { const v = await fetchKey("s:" + id()); Object.assign(draft, v); }, {}); a = () => state.a; }
  else { const m = createMemo(() => fetchKey("s:" + id())); a = () => m().a; }
  const view = () => `${a()} ${isPending(a) ? "pending" : "-"}`;
  const out = where === "boundary" ? untrack(() => createLoadingBoundary(view, () => "loading")) : view;
  createRenderEffect(() => out(), v => { screen = v; });
});
flush();
const show = label => console.log(label.padEnd(18), JSON.stringify(screen));
await tick(); show("mount");
await land("s:1", { a: 1, b: 1 }); show("s(1) lands");
setId(2); flush(); await tick(); show("setId(2)");        // expected: "1 pending"
await land("s:2", { a: 2, b: 2 }); show("s(2) lands");

Found by ReactiveScript's conformance suite, which diffs its spec against Solid's SCENARIOS.md.

Activity

  1. ryansolid commented on Oct 26, 2026

    @ryansolid
    MemberAuthor

    Fixed on next by #4278 (f811097); 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