Repository navigation
Description
Describe the bug
On 2.0.0-rc.16 a <Loading> stays on its fallback forever when all of these hold:
- the
<Loading>sits inside a<Show>whose condition readslatest(() => asyncMemo()) ?? asyncMemo(), so it is mounted by the verdict path; - a component inside it writes an
ownedWritesignal during its mount; - it then creates a memo that reads that signal and returns a promise that never settles.
Nothing reads that memo, and the component renders static text. The docs say only expressions that read an async value wait for it. The signals rules say the same in RULES-FUNDAMENTALS.md, under Reader: "a pending value no render effect reaches holds nothing and reaches no boundary". So the boundary should reveal its content. On rc.13, rc.14 and rc.15 it does.
Your Example Website or App
Self-contained Node + jsdom script below (no compiler, no third-party libraries).
Steps to Reproduce the Bug or Issue
package.json:
{
"private": true,
"type": "module",
"dependencies": { "solid-js": "2.0.0-rc.16", "@solidjs/web": "2.0.0-rc.16", "jsdom": "26.1.0" },
"overrides": { "@solidjs/signals": "2.0.0-rc.16" }
}repro.mjs:
import { JSDOM } from "jsdom";
const dom = new JSDOM("<!doctype html><div id=app></div>");
Object.assign(globalThis, { window: dom.window, document: dom.window.document, Node: dom.window.Node, HTMLElement: dom.window.HTMLElement });
const { createMemo, createSignal, createComponent, Loading, Show, latest } = await import("solid-js");
const { render } = await import("@solidjs/web");
function Child() {
const [version, setVersion] = createSignal(0, { ownedWrite: true });
setVersion(1);
// never read by anything; stays pending forever
createMemo(() => { version(); return new Promise(() => {}); });
return "CHILD";
}
const el = document.getElementById("app");
render(() => {
const gate = createMemo(async () => "x");
return createComponent(Loading, { fallback: "OUTER LOADING", get children() {
return createComponent(Show, { get when() { return latest(() => gate()) ?? gate(); }, children: () =>
createComponent(Loading, { fallback: "INNER LOADING", get children() { return createComponent(Child, {}); } }) });
} });
}, el);
setTimeout(() => { console.log(JSON.stringify(el.textContent)); process.exit(0); }, 300);npm install && node --conditions=browser repro.mjsOutput (identical with --conditions=development):
| solid-js / @solidjs/web / @solidjs/signals | output |
|---|---|
| 2.0.0-rc.13 | "CHILD" |
| 2.0.0-rc.14 | "CHILD" |
| 2.0.0-rc.15 | "CHILD" |
| 2.0.0-rc.16 | "INNER LOADING" (forever) |
With solid-js/web at rc.15 and only @solidjs/signals at rc.16, the output is "INNER LOADING", so the change is in signals.
Expected behavior
"CHILD". The inner boundary should reveal its content. The pending memo has no render-effect reader, so it should not hold the boundary. This is the rc.15 behavior.
Screenshots or Videos
No response
Platform
- OS: macOS
- Runtime: Node 25.8.2 + jsdom 26.1.0 (originally seen in Chromium in an app)
- Version: solid-js / @solidjs/web / @solidjs/signals 2.0.0-rc.16; still reproduces on
next@9f64ca93
Additional context
Works in rc.15, broken in rc.16.
Bisect. I built packages/signals (prod flags) at each signals commit between the rc.15 and rc.16 release merges, with solid-js and @solidjs/web held at rc.15:
- first bad: dc063a6
fix(signals): a verdict-mounted Loading holds a nested render effect's first read (#4166) (#4171); - good:
a2743fa2, the last signals change before it (its direct parent7f8ab2dbonly touches web).
What is needed. Each item below turns the output back to "CHILD" on rc.16:
when={gate()}(nolatest), or a synchronous gate;- writing the signal after creating the pending memo, or not writing it at all;
- the pending memo not reading the written signal;
- doing the write from a
createEffect,createRenderEffector timer instead of during the mount.
A more realistic shape that also reproduces. A data source whose memo starts its load and bumps an ownedWrite version signal on its first compute:
let started = false;
createMemo(() => { version(); if (!started) { started = true; setVersion(1); } return 1; });
createMemo(() => { version(); return new Promise(() => {}); }); // e.g. a disabled query's data: never settles, never readThis is how a query adapter can look: one query starts its fetch, and a sibling disabled query's data projection stays pending. In an app, every screen under such a latest-gated shell stays on the fallback.
Activity
fix(signals): an unread pending memo does not hold a verdict-mounted …
ryansolid commentedon Oct 21, 2026
Generated by Claude Code
fix(signals): an unread pending memo behind memos does not hold a ver…