`lazy()`: remounting an already-loaded component renders empty (or the Suspense fallback) since 1.9.14
Maintainer thường phản hồi trong vòng 1 ngày
@itisrmk đang làm issue này rồi.
Từ ngày 17/10/2026.
Đánh giá
- Độ khó
- 2/5
- Thời gian dự kiến
- 1-3 giờ
- Mức phù hợp với người mới
- 62/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- javascript, typescript
- Lĩnh vực
- frontend
Hướng nghiên cứu
Lỗi nằm ở nhánh client của lazy() trong mã nguồn solid-js (bản build dist/solid.js trỏ tới nhánh này), nơi onCleanup xóa bộ nhớ đệm dùng chung comp khi bất kỳ instance nào bị hủy. Hãy đọc nhánh đó và đường dẫn load() gán comp sau khi giải quyết, rồi kiểm tra xem có test nào hiện có bao phủ việc thử lại một import thất bại không. Hoàn thành khi việc mount lại sau một lần tải thành công render đồng bộ, việc mount lại sau một import thất bại vẫn thử lại, và các test hiện có đều pass.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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
- Ngôn ngữ chính
- TypeScript
- Star
- 36.2k
- Fork
- 1.1k
- Merge trung bình
- 10 giờ 38 phút
- Pull request đã merge (30 ngày)
- 352
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của solidjs/solid
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 52/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
solidjs/solid#4242 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
compiler rc.18: SSR template validation rejects SVG <image> outside an <svg>-rooted templateĐang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 53/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 1-2 ngày Mức phù hợp với người mới 58/100
Maintainer thường phản hồi trong vòng 1 ngày
-
enhancement
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của solidjs/solid
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
awesome-dsh-plugin/awesome-dsh-plugin#9184 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
farion1231/cc-switch#8829 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[core-client] Remove unused hand-maintained transport helpers after the definition-table cutoverĐang mởdoc-audit enhancement
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100