Repository navigation
Description
Describe the bug
Since 2.0.0-rc.14, when a render effect runs depends on how its subtree was opened. A component under a nested <Loading>, inside a <Show>, has a createRenderEffect and reads an async memo. The inner <Loading> shows its fallback in both cases below.
- If the
<Show>opens because a signal changes, the render effect runs at once while the inner<Loading>shows its fallback. This matches rc.13. - If the
<Show>opens because an async memo settles, the render effect does not run until the inner<Loading>'s data resolves. The inner fallback is on screen the whole time.
On rc.13 the render effect runs at once in both cases. createEffect waits for the data in both cases (checked on rc.13 and rc.19), so only render effects changed, and only under the async gate.
This matters for libraries that subscribe from a render effect. @tanstack/solid-query attaches its observer that way. Under the async gate the observer attaches only after the first fetch lands, finds the result stale (with staleTime: 0), and fetches again: every screen behind an async auth gate makes two requests instead of one.
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, createRenderEffect, createSignal, Loading, Show } from "solid-js";
import { expect, it } from "vitest";
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
for (const gate of ["async", "signal"])
it(`render effect under an inner <Loading> runs before its data resolves (gate: ${gate})`, async () => {
const el = document.createElement("div");
const log = [];
function Screen() {
const data = createMemo(async () => { await wait(50); log.push("data"); return "v"; });
createRenderEffect(() => 0, () => { log.push("effect"); });
return <p>{data()}</p>;
}
const [open, setOpen] = createSignal(false);
render(() => {
const ready = gate === "async" ? createMemo(async () => { await wait(10); return true; }) : open;
return (
<Loading fallback="outer">
<Show when={ready()}>
<Loading fallback="inner"><Screen /></Loading>
</Show>
</Loading>
);
}, el);
if (gate === "signal") setTimeout(() => setOpen(true), 10);
await wait(30);
expect(el.textContent).toBe("inner");
expect(log).toEqual(["effect"]);
await wait(100);
expect(log).toEqual(["effect", "data"]);
});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
Both gates behave the same way. With the inner fallback on screen at 30 ms, the render effect has already run (["effect"]), as it does on rc.13, and as it still does on rc.19 under the signal gate.
Actual on rc.14 – rc.19 and on next@17d4d1b (gate: async): log is [] at 30 ms, and the render effect runs only after data resolves.
If deferring render effects behind a fallback is the intended rule now, the signal gate breaks it. In that case, please say which of the two is intended.
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) - Version:
solid-js/@solidjs/web/@solidjs/signals2.0.0-rc.19. Also reproduces onnext@17d4d1b(signals built from source).
Additional context
| solid-js / @solidjs/web / @solidjs/signals | gate: signal |
gate: async |
|---|---|---|
| 2.0.0-rc.13 | runs at once | runs at once |
2.0.0-rc.14 … 2.0.0-rc.19, next@17d4d1b |
runs at once | deferred until the inner data resolves |
Bisect. I built packages/signals from source between the rc.13 and rc.14 release commits, with solid-js and @solidjs/web at rc.14:
- first bad: 1b9ceb6
signals: the hold model (L2) — core rebuilt from its rulings (#3774); - last good signals commit:
4f676973d3.
rc.19's #4302 ("mounts under one boundary keep their own holds across flushes") fixes some related cases, but not this one.
Docs. The Loading reference says the boundary "renders a fallback while pending async reads inside the subtree settle", and the boundaries guide says that "nested boundaries let panels appear as their own first data arrives". Neither says that the way the outer <Show> was opened should change when render effects inside a nested boundary run.
Activity
fix(signals): a render effect's first run goes through under an async…
ryansolid commentedon Oct 28, 2026
Fixed on
nextby #4353 (425a713); it ships in the next RC.Generated by Claude Code