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 reactivity
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 47/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- javascript, typescript
Research direction
Start by running the linked solid-flow reproduction with bunx vitest run src/core/__tests__/showStaleInAction.test.ts; compare its rc.15 failure with the rc.13 result described in REPRO.md. Trace the update ordering around the non-keyed <Show> branch and its child memo in the reproduction. Done means the child is disposed before it re-runs after when becomes undefined, and the regression test passes without halting reactivity.
Written by the indexing model from the issue text.
Description
Summary
solid-js / @solidjs/[email protected] and rc.15. Regression since rc.13.
Inside an open action, a non-keyed <Show when={lookup[id]}> mounts its branch for a row the action added, and later in the same transaction its condition becomes undefined while the <For> that owns it still lists the id. The engine then re-runs a memo inside the branch (one that reads the Show's narrowed accessor) instead of disposing it first. The read throws "Attempting to access a stale value from that could possibly be undefined", and since that memo has no error boundary the whole system stops: REACTIVITY_HALTED, and no further updates are processed. On rc.13 the same sequence never throws (our DOM test asserts the added row is rendered and selected mid-action).
Traced sequence in the failing case (b is the row the action adds):
| step | <For> ids |
<Show when> for b |
b's branch memo |
|---|---|---|---|
| action adds b, selects it | ["a","b"] |
the row | mounts, runs |
| same transaction, later pass | ["a","b"] |
undefined |
RE-RUNS, throws the stale read |
Two things changed between rc.13 and rc.14 here. A derived keyed record drops a row the action added while the id list still has it; that may be an intended effect of rc.14's held writes (the plain-store writes inside the action are now held with the transaction). The re-run of a child under a falsy non-keyed <Show> is the part that looks wrong regardless: the branch should be disposed before its children run again, and a stale read in a child is turned into a crash of the whole graph.
Reproduction (headless, no DOM, no components)
Not yet reduced to @solidjs/signals alone. Inside the select step, each of its four operations is needed (removing any one avoids the crash: the index-memo read, the selected field write, the nested overlay write, the Object.assign). The surrounding pieces are a plain store over a controlled createOptimisticStore (the flow's own node list), an id list subscribed through Reflect.ownKeys, a membership-cadence index memo read untracked inside a setter, per-row createProjections (keyed mapArray) joined with a plain selection-overlay store, a keyed record projection of those rows, and an action that adds a row, then in a second setter field-writes selected on it, nests a write of the overlay entry (holding the row proxy), and Object.assigns the patch. A signals-only model with those pieces does not reproduce yet: there the added row is visible mid-action, where in the library it is held.
The repro uses solid-flow's headless flow state, rendered through solid-js <For> / <Show> with one eager memo per row. Branch repro/solid-rc14-show-stale (commit ec5977d): solid-flow next at addaf7a with the Solid packages at 2.0.0-rc.15, plus the test and a REPRO.md. Setting the Solid packages back to 2.0.0-rc.13 (steps in REPRO.md) makes the same test pass.
git clone -b repro/solid-rc14-show-stale https://github.com/dsnchz/solid-flow && cd solid-flow
bun install
bunx vitest run src/core/__tests__/showStaleInAction.test.ts # fails on rc.15: REACTIVITY_HALTED
const [nodes] = createOptimisticStore<Node[]>([node("a", 0)]);
createRoot(() => {
state = createFlowState({ nodes, edges: [] });
const view = For({
get each() { return state.store.visibleNodeIds; },
children: (id) => Show({
get when() { return state.nodeLookup.get(id); },
children: (row) => {
const label = createMemo(() => `${row().id}:${row().selected}`); // re-runs after `when` went falsy
return () => label();
},
}),
});
createRenderEffect(() => JSON.stringify(resolve(view)), () => {});
});
flush();
const addAndSelect = action(function* () {
state.commands.addNodes(node("b", 200));
state.commands.updateNode("b", { selected: true });
yield gate;
});
solid-js / @solidjs/signals |
result |
|---|---|
| 2.0.0-rc.13 | renders; no error |
| 2.0.0-rc.14 | REACTIVITY_HALTED (stale <Show> read in the row memo) |
| 2.0.0-rc.15 | REACTIVITY_HALTED (stale <Show> read in the row memo) |
On rc.15, without the updateNode call (add only), or selecting an existing row, or updating the new row's data instead of selected, nothing throws.
Where we hit it
@dschz/solid-flow composes flow commands with createOptimisticStore + action (the composition from #3085; our pin for #3665 covers this same add-then-select-in-an-action scenario). In the real component, NodeWrapper's culling memo reads the row through the Show's accessor, so a user action that adds a node and selects it (addNodes then updateNode(id, { selected: true })) before its yield freezes the whole flow on rc.14 and rc.15. We are holding our rc.15 bump on it.
- Dominant language
- TypeScript
- Stars
- 36.1k
- Forks
- 1.1k
- Avg merge
- 11h 45m
- Merged PRs (30d)
- 329
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from solidjs/solid
-
CHEATSHEET.md and RFC 02 still recommend module-scope state for app-wide state (follow-up to #3390)Open
Difficulty 1/5 1-3 hours Newbie friendliness 86/100
Maintainers usually reply within 1 day
-
`lazy()`: remounting an already-loaded component renders empty (or the Suspense fallback) since 1.9.14Possibly taken @itisrmk claimed this 2 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
Maintainers usually reply within 1 day
-
[2.0 / next] Recovering before a held error commits throws the successful valuePossibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 4/5 1-2 days Newbie friendliness 48/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 42/100
Maintainers usually reply within 1 day
Similar issues
-
[测试] 等待仓库加载后再记录多仓库排序基线Possibly taken @myot233 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Maintainers usually reply within 1 day
-
area:arcade p3-medium ready-for-agent testing type:docs
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
fderuiter/deruiter.dev#2369 ·
Maintainers usually reply within 1 day
-
area:devenv origin:agent type:bug
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
overengineeringstudio/effect-utils#1949 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
ccch1mneyyy/dsh-TUI#1631 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
MiniMax-AI/minimax-code#507 ·
Maintainers usually reply within 1 day