Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/routes/(2)concepts/(3)async-reactivity.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,37 @@ Development builds turn that read into an error so it reaches `Errored` instead
Read every reactive input at the top of the function, before the first `await`.
:::

## Stop the work a run started

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.
When a run's cleanups happen depends on whether the user has seen it:

- **A run that never committed** was never on screen, so it is cleaned up as soon as a newer run supersedes it.
An `onCleanup` registered before the first `await` therefore cancels an unfinished request:

```ts
const results = createMemo(async () => {
const q = query();
const controller = new AbortController();
onCleanup(() => controller.abort()); // before the first await
const response = await fetch(`/search?q=${q}`, { signal: controller.signal });
return response.json();
});
```

- **The run on screen** stays fully live until its replacement lands.
Its children keep reacting and its subscriptions and timers keep running, because it is still what the user sees.
Its cleanups run when the replacement commits.

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.
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.

:::pitfall[Do not wait on a cleanup from inside the update that replaces it]
The on-screen run's cleanups run only when the held update commits.
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`.
Signal from the input instead: compare against the changed value, or hand the lock over when the new run acquires it.
:::

## Recap

- Return a promise from a memo and read the result as a plain value; the type is `Product`, not `Promise<Product>` or `Product | undefined`.
Expand All @@ -506,6 +537,7 @@ Read every reactive input at the top of the function, before the first `await`.
- Acknowledge a held update with `isPending` on the content and `latest` on the control the user touched.
- 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.
- Read every reactive input before the first `await`.
- 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.

## Next steps

Expand Down
8 changes: 8 additions & 0 deletions src/routes/(2)concepts/(5)mutations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,14 @@ Make the tentative write inside the `action` that sends the request, and suspend
The store is a plain `createStore`, whose writes are durable even inside an action.
Use `createOptimisticStore` for state that should revert when the request settles.

### The save keeps running after the user leaves the page

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.
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.
Its late writes are harmless, because nothing that was removed still reads them.
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.
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).

### `flush()` inside an action throws `[FLUSH_IN_ACTION]`

An action's writes are held in its transaction until the action settles, so there is nothing for `flush()` to show.
Expand Down
1 change: 1 addition & 0 deletions src/routes/(5)guides/(6)data-fetching-patterns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Three things this code does not do, because Solid does them:
- It does not discard stale responses by hand.
Only the answer to the current question is used; if the response for `mu` arrives after the one for `mug`, it is dropped.
There is no request counter and no `AbortController`.
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).
- It does not blank the list between keystrokes.
The previous results stay visible with the `stale` class until the new ones land.
- It does not show a spinner for an empty query.
Expand Down
Loading