Skip to content

Commit 37fd1e6

Browse files
authored
Merge pull request #3556 from solidjs/fix/loading-on-keyed-boundary
fix(signals,solid): Loading/Errored `on` is a dependency list that re-arms the boundary in the current frame; boundaries are not born held (#3540)
2 parents f16566f + ab51c57 commit 37fd1e6

21 files changed

Lines changed: 1704 additions & 199 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@solidjs/signals": patch
3+
"solid-js": patch
4+
---
5+
6+
`Loading`'s `on` prop is a dependency list, not a key (#3540). The expression is tracked and its value is never compared: a write to anything it reads — plain, optimistic, or a source going pending — **re-arms** the boundary. A re-armed boundary that has something pending under it shows its fallback again; one with nothing pending does nothing (no fallback flash). `latest()` inside `on` is redundant.
7+
8+
The re-arm lands in the **current frame**. A write that makes content pending is held by the readers still showing the old content, and its batch commits when the data lands — but the boundary's swap to its fallback is not part of that batch: it is applied at the flush's finalize, mainline, past any transaction park, so the fallback shows now beside whatever the write is still holding elsewhere on the page. Previously the swap was staged into the pending write's transaction and landed with its commit, by which point the data had arrived and the fallback never showed whenever any other reader of the same data existed (#3524, #3529). The children are not re-created; they stay alive behind the fallback.
9+
10+
`Errored` accepts the same `on`: while it shows its error fallback, a change to a dependency clears the caught error and retries the children (reset keys). `createErrorBoundary` takes `{ on }` as its third argument.
11+
12+
Boundaries are exempt from A29 born-held: a `Loading` mounted while a transaction holds what it reads shows its fallback now (and reveals the staged content at the commit) instead of being born held with the transaction. Born held stays right for a plain memo or effect — published, its value would tear the frame — but a boundary that has not revealed is the exception by definition: its job is to catch what is not ready under it rather than let it hold. This also closes the static-vs-function-child `<Show keyed>` inconsistency from the issue.

‎documentation/solid-2.0/03-control-flow.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ This is primarily intended for use with **stores**, where the data at each index
121121

122122
In 2.0’s async model, async values are part of computations (not a separate `createResource`), so `Loading` is the user-facing “this subtree may be not ready yet” boundary.
123123

124-
`Loading` also accepts an `on` prop to control when the boundary re-shows its fallback during revalidation. See [RFC 05](05-async-data.md) for details.
124+
`Loading` also accepts an `on` prop — a dependency list: a tracked expression whose value is irrelevant; whenever anything it reads changes, the boundary shows its fallback again (if something under it is still pending) instead of keeping stale content. See [RFC 05](05-async-data.md) for details.
125125

126126
### Error boundary: `Errored`
127127

@@ -143,6 +143,15 @@ The reset function is an action: pass it to event handlers or other imperative c
143143
</Errored>
144144
```
145145

146+
`Errored` also accepts an `on` prop, with the same shape as `Loading`'s: a dependency list. While the boundary shows its error fallback, a change to anything `on` reads clears the caught error and retries the children — the same thing `reset()` does, driven by data instead of a click (reset keys):
147+
148+
```jsx
149+
// A navigation retries the page instead of leaving the old route's error up.
150+
<Errored fallback={<ErrorPage />} on={route()}>
151+
<Page />
152+
</Errored>
153+
```
154+
146155
#### Reporting what a boundary caught: the client error hook
147156

148157
A fallback rendered is a failure handled — and, until now, one nothing outside the app could see in production: the browser's global handlers hear what reaches `window.onerror`, and a caught error never does. The client error hook is the prod-tier seam for it — any app that wants to log its errors, not only an APM — and the twin of the server's `configureServerErrors` ([RFC 12](12-ssr-http.md#the-server-error-hook-configureservererrors--onerror)):

‎documentation/solid-2.0/05-async-data.md‎

Lines changed: 32 additions & 21 deletions
Large diffs are not rendered by default.

‎documentation/solid-2.0/MIGRATION.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,7 @@ These APIs are new additions (not renames of 1.x APIs):
725725
- **`affects(target, key?)`** — declare that in-flight work will change the targeted data; the marked record (or the single named slot) reads pending until the transaction settles.
726726
- **`resolve(fn)`** — returns a Promise that resolves when a reactive expression settles.
727727
- **`until(fn, options?)`** — returns a Promise that resolves the first time a reactive predicate settles truthy (with `timeout`/`signal` rejection). Yielded from an `action()`, it holds the transaction — and its optimistic state — open until arriving truth confirms the mutation (live sockets, subscriptions, live queries, or a refetch the action itself issued). The predicate reads the authoritative view: optimistic overrides are invisible (the action's own tentative writes can never satisfy it), while real data — committed or still staged in the open transaction — reads normally. See [RFC 06](06-actions-optimistic.md).
728-
- **`Loading` `on` prop** — controls when a Loading boundary re-shows fallback during revalidation.
728+
- **`Loading` / `Errored` `on` prop** — a dependency list for the boundary: a tracked expression whose value is irrelevant. A change to anything it reads re-arms the boundary — `Loading` shows its fallback again now, in the current frame (if something under it is still pending), instead of keeping stale content through revalidation; `Errored` clears its error and retries the children.
729729
- **`deep(store)`** — deep observation of a store (tracks all nested changes).
730730
- **`reconcile(value, key)`** — diffing function for updating stores from new data.
731731
- **Function-form `createSignal(fn)` / `createStore(fn)`** — derived (writable) primitives.

‎packages/signals/docs/RULES-INDEX.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul
7373
| A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:67` | core.ts×1 scheduler.ts×1 store.ts×1 | action-await-contract.test.ts×2 posture-store-parity.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled 2026-07-17] An ambient transaction window is one flush; parking is flush-driven — (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a trans… |
7474
| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:243` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… |
7575
| A28 | ruled, mechanism landed | `docs/SPEC-ASYNC-SEMANTICS.md:51` | constants.ts×2 core.ts×20 optimistic.ts×2 scheduler.ts×3 types.ts×1 verdict.ts×6 optimistic.ts×3 store.ts×1 | createOptimistic.test.ts×5 latest-held-till-flush.test.ts×1 optimistic-store-layer-scope.test.ts×1 posture-store-parity.test.ts×5 question-scoped-pending.test.ts×3 snapshot-derived-store-rows.test.ts×1 createOptimisticStore.test.ts×10 shallow.test.ts×1 treeshake.test.ts×2 visibility-oracle-store.states.ts×8 visibility-oracle.states.ts×8 | [ruled, mechanism landed 2026-09-15] A write becomes visible at flush — to every channel — (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) **A write becomes visible at flush — to every chan… |
76-
| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:75` | action.ts×1 async.ts×1 core.ts×7 effect.ts×1 optimistic.ts×1 scheduler.ts×1 signals.ts×1 store.ts×3 | body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 direct-commit-readers-posture.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 latest-held-till-flush.test.ts×2 posture-store-parity.test.ts×4 treeshake.test.ts×1 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×5 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … |
76+
| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:75` | boundaries.ts×1 action.ts×1 async.ts×1 core.ts×7 effect.ts×1 optimistic.ts×1 scheduler.ts×1 signals.ts×1 store.ts×3 | body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 direct-commit-readers-posture.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 latest-held-till-flush.test.ts×2 posture-store-parity.test.ts×6 treeshake.test.ts×2 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×5 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … |
7777
| A30 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:207` | async.ts×3 attribution.ts×1 core.ts×2 effect.ts×1 scheduler.ts×4 | async-landing-deps-3461.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 ispending-in-boundary-on-3528.test.ts×1 posture-born-held-and-observation.test.ts×1 treeshake.test.ts×2 write-proposals-3494.test.ts×2 | [ruled 2026-09-13 (#3410)] A memo's dependencies are the committed frame's until the frame is replaced — A pass that _staged_ its value has not replaced the committed frame, so the committed value sti… |
7878
| A31 | live | `docs/SPEC-ASYNC-SEMANTICS.md:83` | core.ts×2 | ispending-combined-atomic-3442.test.ts×1 | [live 2026-09-14 (#3442)] A memo computes under its own lane posture, never its puller's — A memo's value is one shared slot every reader sees, so its pass runs under the lane posture the memo itself … |
7979
| A32 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:91` | core.ts×1 store.ts×1 | visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 | [ruled 2026-09-14] Children-forbidden readers see the frame, not the graph — `createTrackedEffect` and `onSettled` callbacks are effect-phase code that runs after the frame is decided. They read the f… |
80-
| A33 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:223` | boundaries.ts×2 scheduler.ts×1 | async-chain-supersession.test.ts×2 ispending-in-boundary-on-3528.test.ts×2 loading-reset-collects-forwarded-3459.test.ts×3 | [ruled 2026-09-12 (#3375)] A fallback-caught flight holds no transaction; a Loading reset moves the hold onto the boundary — A `<Loading>` boundary showing its fallback is the display of everything un… |
80+
| A33 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:223` | boundaries.ts×2 scheduler.ts×1 | async-chain-supersession.test.ts×2 boundary-not-born-held-3540.test.ts×2 ispending-in-boundary-on-3528.test.ts×2 loading-reset-collects-forwarded-3459.test.ts×3 | [ruled 2026-09-12 (#3375)] A fallback-caught flight holds no transaction; a Loading reset moves the hold onto the boundary — A `<Loading>` boundary showing its fallback is the display of everything un… |
8181
| A34 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:233` | async.ts×1 core.ts×1 scheduler.ts×2 verdict.ts×1 | treeshake.test.ts×1 write-proposals-3494.test.ts×5 | [ruled 2026-09-16 (#3494)] A write is a proposal: one on a held node entangles its tick; one that nets to the committed value is none — A write proposes a value for a node. **Held, both are suggestion… |
8282
## V — fixed violations
8383

0 commit comments

Comments
 (0)