Repository navigation
Description
Describe the bug
This is the case #4203 describes, with one extra memo between the written signal and the never-settling memo. The fix for #4203 (#4207, 0117658bee) does not cover it.
A <Loading> mounted by a keyed <Show> whose condition reads latest(value) ?? value() stays on its fallback forever when the component inside it:
- writes an
ownedWritesignal during mount; - derives a memo from that signal;
- creates a memo that reads the derived memo and returns a promise that never settles.
Nothing reads the pending memo, and the component renders static text. As in #4203, a pending value that no render effect reaches should not hold the boundary.
Your Example Website or App
Inline below: a self-contained vitest + jsdom test, solid-js / @solidjs/web only.
Steps to Reproduce the Bug or Issue
repro.test.jsx:
import { render } from "@solidjs/web";
import { createMemo, createSignal, latest, Loading, Show } from "solid-js";
import { expect, it } from "vitest";
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
it("an unread pending memo behind an intermediate memo does not hold a latest()-mounted <Loading>", async () => {
const el = document.createElement("div");
function Child() {
const [version, setVersion] = createSignal(0, { ownedWrite: true });
setVersion(1);
const gate = createMemo(() => version()); // reading version() directly here passes since #4207
createMemo(() => (gate(), new Promise(() => {}))); // never read, never settles
return <p>child</p>;
}
render(() => {
const value = createMemo(async () => "v");
return (
<Loading fallback={<p>outer</p>}>
<Show when={latest(value) ?? value()} keyed>
{() => <Loading fallback={<p>inner</p>}><Child /></Loading>}
</Show>
</Loading>
);
}, el);
await wait(100);
expect(el.textContent).toBe("child");
});Setup (package.json + vitest.config.mjs)
{
"private": true,
"type": "module",
"dependencies": {
"solid-js": "2.0.0-rc.19", "@solidjs/web": "2.0.0-rc.19", "@solidjs/signals": "2.0.0-rc.19",
"@solidjs/babel-plugin": "2.0.0-rc.19", "@babel/core": "7.28.5",
"vitest": "3.2.4", "vite": "7.1.12", "jsdom": "26.1.0"
},
"pnpm": { "overrides": { "solid-js": "2.0.0-rc.19", "@solidjs/web": "2.0.0-rc.19", "@solidjs/signals": "2.0.0-rc.19" } }
}// vitest.config.mjs
import { transformAsync } from "@babel/core";
import solid from "@solidjs/babel-plugin";
const conditions = ["browser", "development"];
export default {
plugins: [{
name: "solid-jsx", enforce: "pre",
async transform(code, id) {
if (!id.endsWith(".jsx")) return null;
const r = await transformAsync(code, { filename: id, babelrc: false, configFile: false, sourceMaps: true,
plugins: [[solid, { generate: "dom", hydratable: false }]] });
return { code: r.code, map: r.map };
},
}],
resolve: { conditions },
ssr: { resolve: { conditions, externalConditions: conditions }, noExternal: [/solid-js/, /@solidjs\//] },
test: { environment: "jsdom", server: { deps: { inline: [/solid-js/, /@solidjs\//] } } },
};Run: pnpm install && pnpm vitest run repro.test.jsx. Swap all four Solid versions (and the overrides) to test another RC.
Expected behavior
child, as on rc.13 – rc.15. Actual on rc.16 – rc.19 and on next@17d4d1b: inner, forever.
Each of these makes the test pass on rc.19:
- the pending memo reads
version()directly instead ofgate()(the [2.0 rc.16] A<Loading>mounted throughlatest()never reveals while an unread memo stays pending (regression from dc063a65, #4171) #4203 shape, fixed by fix(signals): an unread pending memo does not hold a verdict-mounted Loading (#4203) #4207); when={value()}withoutlatest;- no write, or doing the write from
createEffect/createRenderEffect.
Screenshots or Videos
No response
Platform
- OS: macOS
- Runtime: Node 25.8.2, vitest 3.2.4, jsdom 26.1.0,
@solidjs/babel-plugin(generatedom). Originally seen in Chromium: screens under alatest-gated shell never leave the fallback when a sibling query is disabled. - Version:
solid-js/@solidjs/web/@solidjs/signals2.0.0-rc.19. Also reproduces onnext@17d4d1b(signals built from source).
Additional context
@solidjs/signals (built from source) |
#4203 shape (no gate memo) |
this shape (with gate memo) |
|---|---|---|
5c0d1905d5 (parent of #4207) |
inner |
inner |
0117658bee (#4207) |
child |
inner |
2.0.0-rc.19 (npm), next@17d4d1b |
child |
inner |
Bisect. Signals built from source between the rc.15 and rc.16 release commits, with solid-js and @solidjs/web at rc.16. The first bad commit is dc063a6 fix(signals): a verdict-mounted Loading holds a nested render effect's first read (#4166) (#4171), the same commit as #4203.
Usage. The signal options reference describes ownedWrite as "Suppress dev-mode warnings when writing inside an owned scope". The migration guide says: "Reserve ownedWrite: true for an internal signal that intentionally accepts in-scope writes." A data-fetching adapter is that case. It bumps a version signal while its first compute runs, and a sibling disabled query's data memo derives from it and stays pending.
Activity
fix(signals): an unread pending memo behind memos does not hold a ver…
ryansolid commentedon Oct 28, 2026
Fixed on
nextby #4350 (409f893); it ships in the next RC.Generated by Claude Code