Skip to content

Commit 7c79b33

Browse files
docs: Loading on takes effect only when nothing outside waits on the change
Teach on as a region's offer to leave the update: the update accepts only when that region was the only thing waiting. Show the heading- outside-the-boundary case where on has no visible effect, restate the rule for sibling panels, ask the organizing question about the update rather than the refetch, and add the second cause to the Boundaries common problem, the debugging guide, the data-fetching guide, and the glossary. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4bc3df6 commit 7c79b33

5 files changed

Lines changed: 47 additions & 9 deletions

File tree

src/routes/(2)concepts/(3)async-reactivity.mdx

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,9 @@ The cost is that the fast panel waits for the slow one, and if nothing on the pa
241241
Neither behavior is right for every screen.
242242
A detail page, a form, or a set of totals that must add up should change together.
243243
A set of independent widgets should not have to.
244-
The question to ask about any refetch is one your designer can answer:
244+
The question to ask is about the update, not about any one request, and it is one your designer can answer:
245245

246-
**While this data is being refetched, what should the user see?**
246+
**While this update is in flight, what should the user see?**
247247

248248
| The user should see… | Use |
249249
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
@@ -354,15 +354,41 @@ The `on` prop names the value whose change should make the boundary eligible to
354354
</Loading>
355355
```
356356

357-
When `accountId()` changes and the new account is not ready, this boundary shows its fallback and handles the wait itself instead of holding the update.
358-
The panel no longer keeps the write from committing, so controls outside the boundary see the new id as soon as nothing else in the update is holding it, and the panel shows its skeleton until the new account arrives.
357+
When `accountId()` changes and the new account is not ready, the boundary offers to take its region out of the update: it will show the skeleton and wait on its own, so the rest of the update can commit without it.
358+
If the panel was the only thing waiting, the update accepts.
359+
Controls outside the boundary see the new id at once, the panel shows its skeleton, and the content arrives when the account does.
359360
Pending work caused by other values, such as a refresh of the same account, leaves the content in place as usual.
360361

362+
The update accepts the offer only when the region was the only thing waiting.
363+
An update commits when everything it changed is ready, and `on` does not change that; it only lets one region stop being a reason to wait.
364+
When something outside the boundary is also waiting on the new account, the update stays held, and the region waits inside it with everything else, old content in place and no skeleton:
365+
366+
```tsx
367+
// account is createMemo(() => fetchAccount(accountId()))
368+
369+
// Avoid: the heading reads the same account outside the boundary
370+
<h1>{account().name}</h1>
371+
<Loading on={accountId()} fallback={<AccountSkeleton />}>
372+
<AccountDetails account={account()} />
373+
</Loading>
374+
375+
// Prefer: everything that reads the new account is inside the boundary
376+
<Loading on={accountId()} fallback={<AccountSkeleton />}>
377+
<h1>{account().name}</h1>
378+
<AccountDetails account={account()} />
379+
</Loading>
380+
```
381+
382+
In the `Avoid` version, picking another account looks like nothing happened until the account loads, then the heading and the details change together; the `on` had no visible effect because the heading kept the update waiting.
383+
So the rule for `on` is that nothing outside the boundary may be waiting on the same change.
384+
Enclose every reader of the new subject's data, and when that is not possible, because a breadcrumb, a title, or a sibling panel shows the same subject, drop `on` and acknowledge the wait with `isPending` and `latest` instead.
385+
361386
`on` is a value, not an accessor.
362387
The boundary compares it across updates, so pass `accountId()` rather than `accountId`.
363388

364-
Put `on` on each panel that should show its own placeholder.
365-
A panel without it reads the shared input the normal way and still holds the write, so in the dashboard above, one panel with `on` and two without still waits for the two.
389+
The same rule decides the dashboard above.
390+
Give each panel its own boundary with `on={period()}` and the period write commits at once: the selector flips, every panel shows its skeleton, and each panel's content arrives as its own request lands.
391+
Give `on` to one panel only and the other two, reading the shared input the normal way, still hold the write; the page waits for the slowest request, and the panel with `on` shows no skeleton either.
366392
[Boundaries](/concepts/boundaries) covers placement and how `Reveal` orders several boundaries.
367393

368394
:::deep-dive[A placeholder value instead of a fallback: loadingValue]
@@ -449,7 +475,7 @@ Read every reactive input at the top of the function, before the first `await`.
449475
- Requests are ordered by data dependency, not by component nesting; a waterfall exists only when one request needs another's response.
450476
- After a value has settled, a change to its input is held: the current screen stays visible and everything commits together when the new value lands.
451477
- Acknowledge a held update with `isPending` on the content and `latest` on the control the user touched.
452-
- Put `on={key}` on a `Loading` boundary whose subject changed and should show a placeholder instead of the old content.
478+
- Put `on={key}` on a `Loading` boundary whose subject changed and should show a placeholder instead of the old content; it takes effect only when nothing outside the boundary is waiting on the same change.
453479
- Read every reactive input before the first `await`.
454480

455481
## Next steps

src/routes/(2)concepts/(4)boundaries.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ The `on` prop names the value whose change should make an initialized boundary s
9090
Without it, choosing a new product keeps the old product on screen while the new one loads; with `on={productId()}`, the boundary compares the id across updates and shows the skeleton when the subject changed.
9191
Pending work caused by anything else, such as a refresh of the same product, leaves the content in place.
9292

93+
`on` lets the region stop being a reason for the update to wait; it does not make the update commit.
94+
When something outside the boundary also waits on the new product, such as a heading that reads the same product or a sibling region without `on`, the update stays held and the boundary shows no skeleton.
95+
Enclose every reader of the new subject, or give each sibling its own boundary with `on`; the [Async reactivity](/concepts/async-reactivity#show-a-placeholder-again-loading-on) page shows both cases.
96+
9397
:::pitfall[Passing the accessor to on instead of its value]
9498
`on` is compared with `!==` across updates, so pass a primitive; join several inputs into one string rather than passing an array.
9599

@@ -255,6 +259,10 @@ The request can stay where it was; the [Async reactivity](/concepts/async-reacti
255259
That is the default: after a first answer, the boundary keeps its content and the update is held.
256260
Add `on={id()}` to the boundary when a change of subject should show the fallback, and pass the value rather than the accessor.
257261

262+
If `on` is already there and the old item still stays, something outside the boundary is waiting on the same change: a title reading the same item, or a sibling boundary without `on`.
263+
The update cannot commit until that reader is ready, so the fallback never shows.
264+
Move the boundary out to enclose the other reader, give the sibling its own `on`, or drop `on` and show the wait with `isPending`.
265+
258266
### Retry shows the same error
259267

260268
`reset` re-runs the failed sources with the same inputs.
@@ -271,6 +279,7 @@ Give the group `order="natural"`, wrap the independent regions in a nested `<Rev
271279
- Put `Loading` around the smallest region its fallback should replace, and keep the controls the user needs outside it.
272280
- Put `Errored` around the smallest region that can fail and recover as a unit; both boundaries can wrap the same region.
273281
- After a first answer, `Loading` keeps its content during updates; add `on={key()}` when a changed subject should show the fallback again, and pass a value, not an accessor.
282+
It takes effect only when nothing outside the boundary is waiting on the same change.
274283
- An errored region recovers when an input changes or a refresh succeeds; `reset` is for when nothing upstream will change.
275284
- An error thrown by a fallback needs a boundary above it.
276285
- Use `Reveal` to control the order sibling regions appear in; it changes timing, not what each boundary observes.

src/routes/(5)guides/(10)debugging-reactivity.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ The repair is always to add feedback, never to remove the hold:
263263

264264
A `Loading` boundary that has not shown content yet, or whose `on` value changed, is a different situation: the read shows the fallback instead of entering a hold, so there is no `SILENT_HOLD` to report.
265265
A boundary that has already revealed holds like everything else.
266+
So does a boundary with `on` when something outside it waits on the same change: the update is held regardless, the fallback never shows, and the report names the source the click waited on; look for a reader of that source outside the boundary.
266267
A hold that was acknowledged but still ran long is reported as `[LONG_HOLD]` from 500ms of waiting, a warning from 1000ms, with the suggestion to add a `Loading on={...}` boundary; the thresholds are `attribution.enable({ longHolds: { infoMs, warnMs } })`.
267268
Shorter acknowledged holds are recorded as `late` in the feedback tables so the cost is visible without blaming code that waited correctly.
268269

src/routes/(5)guides/(6)data-fetching-patterns.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ If the page should not reveal out of order, wrap the boundaries in [`Reveal`](/r
133133

134134
When `props.id` changes, all three requests start again and the update is held until all three have answered; the page then swaps as one.
135135
That is usually what you want for a detail page, where the header and the reviews must describe the same product.
136-
If a slow section should not delay the others on a subject change, give it `on={props.id}` and it will show its skeleton instead of holding the rest.
136+
If a slow section should not delay the others on a subject change, give it `on={props.id}`: it stops holding the update, so the page swaps as soon as the other two have answered, and the slow section shows its skeleton from that swap until its own data lands.
137+
Give all three `on` and the page swaps on the click itself, each section on its skeleton until its data arrives.
138+
The [Async reactivity](/concepts/async-reactivity#show-a-placeholder-again-loading-on) page explains why `on` only helps when nothing outside its boundary is waiting on the same change.
137139

138140
## Dependent requests
139141

src/routes/(7)glossary.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ See [Server and client boundaries](/concepts/rendering-and-ssr#server-and-client
284284
### Loading boundary
285285

286286
`Loading` renders its fallback while an async value read in its subtree has not produced a first answer.
287-
Once it has shown content it keeps that content during later updates; the `on` prop names the value whose change should bring the fallback back.
287+
Once it has shown content it keeps that content during later updates; the `on` prop names the value whose change should bring the fallback back, which happens only when nothing outside the boundary is waiting on the same change.
288288

289289
Also called: `Suspense`.
290290

0 commit comments

Comments
 (0)