Repository navigation
Description
Describe the bug
Since 2.0.0-rc.10 an update that creates a new subtree never commits when, inside that subtree, isPending() of the same async memo is read from two places (here: a <Show> nested in one <Errored> and a <Show> in a sibling <Errored>) and that memo lands after another async memo of the same subtree. The old subtree stays on screen forever, no error is thrown, the reactive system is not halted (later unrelated writes still work), the new section just never lands.
The same tree mounts fine on first render. Only the re-creation inside an update (a keyed <For> row swap in the repro, a route/param change in the real app) hangs. The hang goes away with any one of:
- removing the second
isPending(() => slow())reader (only one boundary reads it), - making the twice-read memo land first (
fastslow,slowfast), - reading
isPendingonce in an effect and fanning it out through a signal (our workaround).
2.0.0-rc.9 passes the repro; rc.10, rc.11, rc.13, rc.15, rc.17 and rc.19 all hang (bisected with the matching @solidjs/web and @solidjs/vite-plugin for each).
We hit this in an app where a team picker swaps a section that fetches the standings and the matches of the selected group: the switch commits or hangs depending on which of the two requests answers first.
Your Example Website or App
Minimal test (vitest + jsdom + @solidjs/testing-library), fails on rc.19, passes on rc.9:
import { fireEvent, render, screen, waitFor } from "@solidjs/testing-library";
import { createMemo, createSignal, Errored, For, isPending, latest, Loading, Show } from "solid-js";
import { expect, it } from "vitest";
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const latestOr = <T,>(read: () => T, fallback: T): T => {
try {
return latest(read) ?? fallback;
} catch {
return fallback;
}
};
function Section(props: { k: string }) {
const fast = createMemo(async () => {
await sleep(5);
return "fast-" + props.k;
});
const slow = createMemo(async () => {
await sleep(30);
return "slow-" + props.k;
});
return (
<div>
<Errored fallback={<i>err1</i>}>
<Show when={!isPending(() => fast())}>
<p>{latestOr(() => fast(), "")}</p>
<Show when={!isPending(() => slow())}>
<b data-testid="slow">{latestOr(() => slow(), "")}</b>
</Show>
</Show>
</Errored>
{/* second reader of the same memo in a sibling boundary: remove it and the update commits */}
<Errored fallback={<i>err2</i>}>
<Show when={!isPending(() => slow())}>
<span>side</span>
</Show>
</Errored>
</div>
);
}
it("commits the re-created section", async () => {
const [key, setKey] = createSignal("a");
const list = createMemo(() => [key()]);
render(() => (
<Loading fallback={<i>loading</i>}>
<button type="button" onClick={() => setKey("b")}>toggle</button>
<For each={list()}>{(k) => <Section k={k} />}</For>
</Loading>
));
await waitFor(() => expect(screen.getByTestId("slow")).toHaveTextContent("slow-a"));
fireEvent.click(screen.getByText("toggle"));
// rc.9: passes. rc.10 … rc.19: times out, the DOM still shows slow-a.
await waitFor(() => expect(screen.getByTestId("slow")).toHaveTextContent("slow-b"), { timeout: 1500 });
});The result is the same without the <Loading> boundary (the ASYNC_OUTSIDE_LOADING_BOUNDARY path).
Steps to Reproduce the Bug or Issue
- Put the test above in a Vite + vitest (jsdom) project with
solid-js@2.0.0-rc.19,@solidjs/web@2.0.0-rc.19,@solidjs/vite-plugin@3.0.0-next.51,@solidjs/testing-library@1.0.0-rc.2. - Run it: the second
waitFortimes out;screen.debug()shows the old row (slow-a) still mounted. - Delete the second
<Errored>block (the sibling reader ofslow) or swap the two sleep durations: the test passes. - Downgrade to
solid-js@2.0.0-rc.9/@solidjs/web@2.0.0-rc.9/@solidjs/vite-plugin@3.0.0-next.44: the test passes unchanged.
Expected behavior
The new Section commits once both of its async memos have settled, independent of how many boundaries read isPending of each memo and of the order in which the memos land, as it did on rc.9 and as it does on first mount.
Screenshots or Videos
No response
Platform
- OS: macOS 27
- Node: 24.16
- solid-js / @solidjs/web: 2.0.0-rc.19 (regression since rc.10)
- @solidjs/vite-plugin: 3.0.0-next.51
- vitest 5.0.5, jsdom 30.1.3, @solidjs/testing-library 1.0.0-rc.2
Additional context
Workaround that we ship: read isPending once per memo in a createEffect and mirror it into createSignals that the <Show>s gate on. One memo read many times inside a single boundary does not trigger it, and a section with only one async memo does not either.
Activity
fix(signals): a leaf waiting on an unshown lane re-derives at its rev…
ryansolid commentedon Oct 28, 2026
Fixed on
nextby #4347 (cddc7c3); it ships in the next RC.Generated by Claude Code