`lazy()`: remounting an already-loaded component renders empty (or the Suspense fallback) since 1.9.14
I maintainer di solito rispondono entro 1 giorno
@itisrmk ci sta già lavorando.
Dal 17/10/2026.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 62/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- javascript, typescript
- Ambito
- frontend
Direzione di ricerca
Il bug si trova nel ramo client di lazy() nel codice sorgente di solid-js (la build dist/solid.js punta a esso), dove un onCleanup svuota la cache condivisa comp quando viene distrutta una qualsiasi istanza. Leggi quel ramo e il percorso load() che imposta comp dopo la risoluzione, poi verifica se qualche test esistente copre il nuovo tentativo di un import fallito. È completato quando un rimontaggio dopo un caricamento riuscito esegue il rendering in modo sincrono, un rimontaggio dopo un import fallito riprova ancora e i test esistenti passano.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Describe the bug
Since 1.9.14, disposing any instance of a lazy() component clears the module-level component cache shared by all instances. The next mount of the same, already-loaded component creates a new resource and renders "" until it resolves. Inside <Suspense>, the fallback shows again. In 1.9.13 the remount renders the component synchronously.
This shows up as a visible flash whenever a lazily loaded page or overlay is closed and opened again.
Your Example Website or App
https://playground.solidjs.com/anonymous/08d6df4c-9789-4c96-8910-bb818f3fb391
Steps to Reproduce the Bug or Issue
Run test with solid version 1.9.17 from playground url. If you test it locally with solid 1.9.13 works as expected
import { createRoot, lazy, Suspense } from "solid-js";
import { render } from "solid-js/web";
import { createSignal } from "solid-js";
const flush = () => new Promise<void>((r) => setTimeout(r, 0));
function App() {
const [lines, setLines] = createSignal<{ text: string; pass: boolean }[]>([]);
async function runTest() {
const out: { text: string; pass: boolean }[] = [];
const check = (label: string, actual: unknown) => {
const pass = actual === "loaded";
out.push({ text: `${label}: ${JSON.stringify(actual)}`, pass });
};
// ── Case 1: without Suspense ──────────────────────────────
const Lazy1 = lazy(() => Promise.resolve({ default: () => "loaded" }));
let first1 = createRoot((d) => ({ d, v: Lazy1({}) }));
await flush();
first1.d();
let second1 = createRoot((d) => ({ d, v: Lazy1({}) }));
const r1 =
typeof second1.v === "function" ? (second1.v as any)() : second1.v;
check("without Suspense – remount", r1);
second1.d();
// ── Case 2: with Suspense ─────────────────────────────────
const Lazy2 = lazy(() => Promise.resolve({ default: () => "loaded" }));
const mkView = () =>
Suspense({
fallback: "fallback",
get children() {
return Lazy2({});
},
});
let first2 = createRoot((d) => ({ d, v: mkView() }));
await flush();
first2.d();
let second2 = createRoot((d) => ({ d, v: mkView() }));
const r2 =
typeof second2.v === "function" ? (second2.v as any)() : second2.v;
check("with Suspense – remount", r2);
second2.d();
setLines(out);
}
return (
<div style="font-family:monospace;padding:1rem">
<h2>lazy() remount regression · solid-js 1.9.14–1.9.17</h2>
<p>
Expected: <code>"loaded"</code> both times (solid-js 1.9.13 ✅)
</p>
<button onClick={runTest}>▶ Run test</button>
<ul>
{lines().map((l) => (
<li style={`color:${l.pass ? "green" : "red"}`}>
{l.pass ? "✅" : "❌"} {l.text}
</li>
))}
</ul>
</div>
);
}
render(() => <App />, document.getElementById("app")!);
Expected behavior
Once the module has loaded, a remount renders the component synchronously, as in 1.9.13:
== solid-js 1.9.13
without Suspense first mount after load: "loaded", remount: "loaded"
with Suspense first mount after load: "loaded", remount: "loaded"
Actual, in 1.9.14 through 1.9.17:
== solid-js 1.9.14
without Suspense first mount after load: "loaded", remount: ""
with Suspense first mount after load: "loaded", remount: "fallback"
== solid-js 1.9.17
without Suspense first mount after load: "loaded", remount: ""
with Suspense first mount after load: "loaded", remount: "fallback"
Cause
1.9.14 added an onCleanup in the client branch of lazy() (dist/solid.js):
} else if (!comp) {
const [s] = createResource(() => (p || (p = fn())).then(mod => mod.default));
comp = s;
+ onCleanup(() => comp = undefined);
}
comp is shared by every instance of the lazy() component, but the cleanup is registered on the owner of whichever instance created the resource. Disposing that one instance therefore clears the cache for all of them, even after the module has loaded. The next mount takes the !comp branch again and waits for a new resource. The module promise p is still cached, so nothing is re-downloaded, but the new resource resolves asynchronously, which causes the empty render or Suspense fallback.
Possible fix
If the cleanup is meant to let a failed import be retried on the next mount, it could clear the cache only when it still points at this instance's resource and that resource hasn't loaded. For example, against 1.9.17, where load() already sets comp = () => mod.default once the module resolves:
- onCleanup(() => comp = undefined);
+ onCleanup(() => comp === s && (comp = undefined));
With this change, a remount after a successful load renders synchronously, and a remount after a failed import still retries.
Screenshots or Videos
No response
Platform
- OS: [macOS]
- solid-js: 1.9.14 to 1.9.17 (works in 1.9.13)
- Client build (
dist/solid.cjs/dist/solid.js), no SSR or hydration - Node 22 for the reproduction; we first saw it in a SolidJS app using the universal renderer
Additional context
No response
- Lingua principale
- TypeScript
- Stelle
- 36.1k
- Fork
- 1.1k
- Merge medio
- 11h 45m
- PR unite (30g)
- 329
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di solidjs/solid
-
CHEATSHEET.md and RFC 02 still recommend module-scope state for app-wide state (follow-up to #3390)Aperta
Difficoltà 1/5 1-3 ore Idoneità per principianti 86/100
I maintainer di solito rispondono entro 1 giorno
-
[2.0 / next] Recovering before a held error commits throws the successful valueForse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta
Difficoltà 4/5 1-2 giorni Idoneità per principianti 48/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 42/100
I maintainer di solito rispondono entro 1 giorno
-
rc.14: inside an action, a non-keyed `<Show>`'s child memo re-runs after its condition turned falsy, and the stale-value throw halts reactivityForse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta
Difficoltà 4/5 3-5 giorni Idoneità per principianti 47/100
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di solidjs/solid
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
ccch1mneyyy/dsh-TUI#1631 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
MiniMax-AI/minimax-code#507 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
-
Transient OAuth login failure persists in getLoginStatus and masks active account in dashboardForse di nuovo libera Una pull request per questa issue è stata chiusa senza essere unita. Apertaaccount-pool bug gui
Difficoltà 2/5 1-3 ore Idoneità per principianti 67/100
lidge-jun/opencodex#7378 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Admin search throws for permissions added through Admin.generalIndexItems('permissions', ...)Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
I maintainer di solito rispondono entro 1 giorno