Repository navigation
Description
Reproduced on next at 2a0da2f (2026-10-24), with @solidjs/signals, solid-js and @solidjs/universal built from source. Run: node --conditions=browser --conditions=development <script>.
Rule. SCENARIOS.md S-22 row 3′ (D-18): "if content(2) lands before
meta(2) → content(2) — the fallback is never seen." Pinned by
tests/loading-on-rearm-reveal-3540.test.ts "content lands before the
fallback: the fallback is never seen".
Shape. An outer Loading, inside it a Loading with on: id whose
fallback reads async meta and whose content reads async content; both
memos depend on id. After the first load, setId(2); content(2) lands
before meta(2).
Observed. If the fallback creates a render effect that reads meta —
which is what any compiled JSX fallback does: <p>Meta {meta()}</p> is an
element plus an insert effect — the screen keeps content(1) after
content(2) lands, and reveals content(2) only when meta(2) lands:
effect-in-fallback read-in-fallback (the pinned shape)
setId(2) "content c1" "content c1"
content(2) lands "content c1" ✗ "content c2" ✓
meta(2) lands "content c2" "content c2"
dv3-web.mjs shows the same through solid-js's <Loading> and
@solidjs/universal: an element fallback fails, an array fallback
(["Meta ", () => meta()]) and a static fallback pass.
Likely cause. The fallback's render effect is a frame reader that is
pending on meta(2) and counts toward the change's hold, though the
fallback is never the display in this race (a reader behind content
that is not shown should hold nothing). The pinned test reads fallbackData()
directly in the fallback function, so no render effect exists and the
case is not covered.
Repro
dv3-signals.mjs (effect-in-fallback | read-in-fallback)
// signals only: a re-armed boundary whose fallback reads pending data; the content lands first.
// Expected (SCENARIOS.md S-22 row 3′, loading-on-rearm-reveal-3540 "content lands before the fallback"): content(2), fallback never seen.
import { createRoot, createSignal, createMemo, createRenderEffect, createLoadingBoundary, 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 land = async (key, v) => { flights.get(key).resolve(v); await new Promise(r => setTimeout(r, 0)); flush(); await new Promise(r => setTimeout(r, 0)); flush(); };
let screen = "";
const [id, setId] = createSignal(1);
createRoot(() => {
const meta = createMemo(() => fetchKey("m:" + id()));
const content = createMemo(() => fetchKey("c:" + id()));
const variant = process.argv[2] ?? "effect-in-fallback";
const fallback = variant === "read-in-fallback"
? () => `Meta ${meta()}` // the shape Solid's pinned test uses: passes
: () => { let t = "Meta"; createRenderEffect(() => meta(), v => { t = `Meta ${v}`; }); return t; }; // a render effect inside the fallback (what an element + insert is)
const inner = untrack(() => createLoadingBoundary(() => `content ${content()}`, fallback, { on: id }));
const outer = untrack(() => createLoadingBoundary(() => inner(), () => "outer"));
createRenderEffect(() => outer(), v => { screen = v; });
});
flush();
const show = label => console.log(label.padEnd(26), JSON.stringify(screen));
show("mount");
await land("m:1", 1); show("meta(1) lands");
await land("c:1", "c1"); show("content(1) lands");
setId(2); flush(); await new Promise(r => setTimeout(r, 0)); flush(); show("setId(2)");
await land("c:2", "c2"); show("content(2) lands FIRST"); // expected: "content c2"
await land("m:2", 2); show("meta(2) lands");dv3-web.mjs (element-fallback | array-fallback | static-fallback)
// through solid-js's <Loading> and a renderer: a re-armed boundary whose fallback reads pending data; the content lands first.
// Expected (SCENARIOS.md S-22 row 3′): after content(2) lands, the screen shows c2 and the fallback is never seen.
import { createRenderer } from "@solidjs/universal";
import { Loading } from "solid-js";
import { createSignal, createMemo, flush } from "@solidjs/signals";
// a string-tree renderer
const R = createRenderer({
createElement: tag => ({ tag, children: [] }), createTextNode: text => ({ text }),
replaceText: (n, v) => { n.text = v; }, isTextNode: n => "text" in n, setProperty() {},
insertNode: (p, n, a) => { if (n.parent) n.parent.children.splice(n.parent.children.indexOf(n), 1); const i = a ? p.children.indexOf(a) : -1; i < 0 ? p.children.push(n) : p.children.splice(i, 0, n); n.parent = p; },
removeNode: (p, n) => { const i = p.children.indexOf(n); if (i >= 0) p.children.splice(i, 1); n.parent = undefined; },
getParentNode: n => n.parent, getFirstChild: n => n.children?.[0], getNextSibling: n => n.parent?.children[n.parent.children.indexOf(n) + 1],
});
const text = n => "text" in n ? n.text : n.children.map(text).join("");
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 [id, setId] = createSignal(1);
const root = { tag: "root", children: [] };
const variant = process.argv[2] ?? "element-fallback";
R.render(() => {
const meta = createMemo(() => fetchKey("m:" + id()));
const content = createMemo(() => fetchKey("c:" + id()));
const inner = () => R.createComponent(Loading, {
get on() { return id(); },
get fallback() {
if (variant === "static-fallback") return R.createTextNode("Meta");
if (variant === "array-fallback") return [R.createTextNode("Meta "), () => String(meta())];
const p = R.createElement("p"); R.insert(p, [R.createTextNode("Meta "), () => String(meta())]); return p; // what <p>Meta {meta()}</p> compiles to
},
get children() { return () => String(content()); },
});
return R.createComponent(Loading, { get fallback() { return R.createTextNode("outer"); }, get children() { return inner(); } });
}, root);
flush();
const show = label => console.log(label.padEnd(26), JSON.stringify(text(root)));
show("mount"); await tick(); show("mount (settled)");
await land("m:1", 1); show("meta(1) lands");
await land("c:1", "c1"); show("content(1) lands");
setId(2); flush(); await tick(); show("setId(2)");
await land("c:2", "c2"); show("content(2) lands FIRST"); // expected: "c2"
if (flights.has("m:2")) { await land("m:2", 2); show("meta(2) lands"); }Found by ReactiveScript's conformance suite, which diffs its spec against Solid's SCENARIOS.md.
Activity
fix(signals): dispose a never-shown Loading fallback at once instead …
chore(size): raise caps for #4268's +24 B (Size-Exception)
ryansolid commentedon Oct 26, 2026
Fixed on
nextby #4282 (3b6f839); it ships in the next RC.Generated by Claude Code
fix(signals): a re-armed Loading's off-screen fallback no longer hold…