You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 5e33ea2
Browse filesBrowse the repository at this point in the historyBrowse files
docs: when async work stops, and actions that outlive their page (#1668)
Adds a "Stop the work a run started" section to the async concept page:
a never-committed run is cleaned up when superseded (so onCleanup +
AbortController cancels an unfinished request), the run on screen stays
live until its replacement lands, and waiting on that cleanup from inside
the replacing update can deadlock. Mutations gains a common-problems entry
for a save that keeps running after navigation (by design; guard post-yield
steps, don't retire actions from onCleanup). Follows solid #4222 / #4223.
Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: src/routes/(2)concepts/(3)async-reactivity.mdx
+32Lines changed: 32 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -496,6 +496,37 @@ Development builds turn that read into an error so it reaches `Errored` instead
496
496
Read every reactive input at the top of the function, before the first `await`.
497
497
:::
498
498
499
+
## Stop the work a run started
500
+
501
+
Solid drops a superseded run's result on its own, but the work the run started, such as a request, a timer, or a subscription, keeps going unless the run stops it.
502
+
When a run's cleanups happen depends on whether the user has seen it:
503
+
504
+
-**A run that never committed** was never on screen, so it is cleaned up as soon as a newer run supersedes it.
505
+
An `onCleanup` registered before the first `await` therefore cancels an unfinished request:
506
+
507
+
```ts
508
+
const results =createMemo(async () => {
509
+
const q =query();
510
+
const controller =newAbortController();
511
+
onCleanup(() =>controller.abort()); // before the first await
-**The run on screen** stays fully live until its replacement lands.
518
+
Its children keep reacting and its subscriptions and timers keep running, because it is still what the user sees.
519
+
Its cleanups run when the replacement commits.
520
+
521
+
An iterable that the computation returns is different: it is closed as soon as the run is superseded, because nothing can show what it yields any more.
522
+
To stop work when one input changes, tie that work to the input in a child computation rather than to the outer one, so it re-runs with the input instead of waiting for the whole update.
523
+
524
+
:::pitfall[Do not wait on a cleanup from inside the update that replaces it]
525
+
The on-screen run's cleanups run only when the held update commits.
526
+
Anything inside that update that waits for one of those cleanups can wait forever: an action that races a promise resolved from a memo's `onCleanup`, or a new run that awaits a lock the old run releases in `onCleanup`.
527
+
Signal from the input instead: compare against the changed value, or hand the lock over when the new run acquires it.
528
+
:::
529
+
499
530
## Recap
500
531
501
532
- Return a promise from a memo and read the result as a plain value; the type is `Product`, not `Promise<Product>` or `Product | undefined`.
@@ -506,6 +537,7 @@ Read every reactive input at the top of the function, before the first `await`.
506
537
- Acknowledge a held update with `isPending` on the content and `latest` on the control the user touched.
507
538
- Put `on={key()}` on a `Loading` boundary whose subject changed and should show a placeholder instead of the old content; `on` is tracked, and the fallback never appears when the same data is also read outside the boundary.
508
539
- Read every reactive input before the first `await`.
540
+
- Cancel a request with `onCleanup` and an `AbortController` registered before the first `await`; a run that never committed is cleaned up when it is superseded, and the run on screen when its replacement lands.
Copy file name to clipboardExpand all lines: src/routes/(2)concepts/(5)mutations.mdx
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -341,6 +341,14 @@ Make the tentative write inside the `action` that sends the request, and suspend
341
341
The store is a plain `createStore`, whose writes are durable even inside an action.
342
342
Use `createOptimisticStore` for state that should revert when the request settles.
343
343
344
+
### The save keeps running after the user leaves the page
345
+
346
+
An action is not tied to the component that created it, so navigating away does not cancel it and the save still lands, like a form submit.
347
+
That is the safe default: the action is often what navigates (save, then redirect), and cancelling it there would undo the write the user asked for.
348
+
Its late writes are harmless, because nothing that was removed still reads them.
349
+
Guard the steps after a `yield` that only make sense on the old page, for example by comparing against the current record id, and pass an `AbortSignal` to the request when it should really stop.
350
+
Do not retire the action from a component's `onCleanup`; see [Stop the work a run started](/concepts/async-reactivity#stop-the-work-a-run-started).
351
+
344
352
### `flush()` inside an action throws `[FLUSH_IN_ACTION]`
345
353
346
354
An action's writes are held in its transaction until the action settles, so there is nothing for `flush()` to show.
Copy file name to clipboardExpand all lines: src/routes/(5)guides/(6)data-fetching-patterns.mdx
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -82,6 +82,7 @@ Three things this code does not do, because Solid does them:
82
82
- It does not discard stale responses by hand.
83
83
Only the answer to the current question is used; if the response for `mu` arrives after the one for `mug`, it is dropped.
84
84
There is no request counter and no `AbortController`.
85
+
The superseded request still finishes in the background; to cancel it, see [Stop the work a run started](/concepts/async-reactivity#stop-the-work-a-run-started).
85
86
- It does not blank the list between keystrokes.
86
87
The previous results stay visible with the `stale` class until the new ones land.
0 commit comments