From 73ebd66aea6b37b0f7f2d2c357acf65a12d19542 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 09:05:34 +0530 Subject: [PATCH 1/4] docs: fix the from() migration snippet, add createDeferred to the cheatsheet, note SETTLED_CLEANUP_UNOWNED throws --- documentation/solid-2.0/MIGRATION.md | 11 +++++------ packages/solid/CHEATSHEET.md | 6 ++++++ packages/solid/skills/reactivity-diagnostics/SKILL.md | 3 ++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/documentation/solid-2.0/MIGRATION.md b/documentation/solid-2.0/MIGRATION.md index af0b280069..bb87e95071 100644 --- a/documentation/solid-2.0/MIGRATION.md +++ b/documentation/solid-2.0/MIGRATION.md @@ -1001,14 +1001,13 @@ The key difference: writes go through `setState`, which makes them visible to th import { from } from "solid-js"; const signal = from(observable$); -// 2.0 — async iterables are first-class in computations -const value = createMemo(async function* () { - for await (const val of observable$) { - yield val; - } -}); +// 2.0 — async iterables are first-class in computations. Return the iterable +// itself: disposing the owner calls its return() and unsubscribes at once. +const value = createMemo(() => observable$); ``` +Avoid wrapping the source in an `async function*` with `for await`. While the generator waits at an `await`, its `return()` is queued, so the upstream subscription stays open after disposal until the source produces another value. A source that isn't an async iterable (a callback subscription, an Observable) needs an adapter whose `return()` unsubscribes synchronously (see #4182). + **Solid → External (`observable`):** There's no drop-in replacement. `observable()` produced a standard Observable that external libraries could subscribe to. In 2.0, use `createEffect` to push signal changes to an external subscriber: ```js diff --git a/packages/solid/CHEATSHEET.md b/packages/solid/CHEATSHEET.md index f596d7f020..9a6d98bcfc 100644 --- a/packages/solid/CHEATSHEET.md +++ b/packages/solid/CHEATSHEET.md @@ -33,6 +33,7 @@ import { createOptimisticStore, isPending, latest, + createDeferred, refresh, affects, untrack, @@ -225,6 +226,11 @@ const user = createMemo(() => fetchUser(id())); // Peek at the in-flight value during a transition latest(id); +// A memo that may lag but never leads (React's useDeferredValue): after its +// first answer, a refetch keeps the last value on screen instead of holding +// the change; isPending(() => rows()) reports the refetch. +const rows = createDeferred(() => fetchRows(period())); + // Force recompute of a derived read after a server write. // Action: call from handlers/effects/actions, not pure reads. // A bare refresh() re-asks the SAME question — it is quiet (isPending stays diff --git a/packages/solid/skills/reactivity-diagnostics/SKILL.md b/packages/solid/skills/reactivity-diagnostics/SKILL.md index e1e211d4d7..77d70355f5 100644 --- a/packages/solid/skills/reactivity-diagnostics/SKILL.md +++ b/packages/solid/skills/reactivity-diagnostics/SKILL.md @@ -173,7 +173,8 @@ return a cleanup function from the callback instead. ### SETTLED_CLEANUP_UNOWNED `onSettled` returned a cleanup while running in an unowned scope, so the -cleanup cannot be honored. Call the setup helper from an owned scope (the +cleanup cannot be honored. Throws in dev; in production the cleanup is +dropped. Call the setup helper from an owned scope (the component body), not from an event handler, tracked effect, or another `onSettled`. From 5e970d8e4f7fd1f1df603336c1eba0586251380d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 09:23:00 +0530 Subject: [PATCH 2/4] docs(migration): show a from() replacement that keeps logic in the memo and still closes the source --- documentation/solid-2.0/MIGRATION.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/documentation/solid-2.0/MIGRATION.md b/documentation/solid-2.0/MIGRATION.md index bb87e95071..15394552bb 100644 --- a/documentation/solid-2.0/MIGRATION.md +++ b/documentation/solid-2.0/MIGRATION.md @@ -1006,7 +1006,32 @@ const signal = from(observable$); const value = createMemo(() => observable$); ``` -Avoid wrapping the source in an `async function*` with `for await`. While the generator waits at an `await`, its `return()` is queued, so the upstream subscription stays open after disposal until the source produces another value. A source that isn't an async iterable (a callback subscription, an Observable) needs an adapter whose `return()` unsubscribes synchronously (see #4182). +Solid calls the returned iterator's `return()` when the owner is disposed and when a re-run replaces the stream. When the memo needs logic around the source (mapping, filtering, picking the source from a signal), an `async function*` is the natural fit, but don't hand it to Solid as-is. While the generator waits at an `await`, its `return()` is queued, so the source stays subscribed until it produces another value. Close the source yourself in `return()`: + +```js +const value = createMemo(() => { + // Read signals here, before any await, so they are tracked. + const source = feedFor(id())[Symbol.asyncIterator](); + const scale = factor(); + const gen = (async function* () { + for (let r; !(r = await source.next()).done; ) { + if (r.value.visible) yield r.value.amount * scale; + } + })(); + return { + [Symbol.asyncIterator]: () => ({ + next: () => gen.next(), + // Close the source at once; the generator's own return() waits for its await. + return: () => { + source.return?.(); + return gen.return(); + } + }) + }; +}); +``` + +A source that isn't an async iterable (a callback subscription, an Observable) needs an adapter whose `return()` unsubscribes synchronously (see #4182). **Solid → External (`observable`):** There's no drop-in replacement. `observable()` produced a standard Observable that external libraries could subscribe to. In 2.0, use `createEffect` to push signal changes to an external subscriber: From e321b90d70f1e061f165a43145e4d6c8126dc217 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 09:25:12 +0530 Subject: [PATCH 3/4] docs(migration): keep the async generator from() replacement and note its delayed cleanup --- documentation/solid-2.0/MIGRATION.md | 34 +++++----------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/documentation/solid-2.0/MIGRATION.md b/documentation/solid-2.0/MIGRATION.md index 15394552bb..9f214499eb 100644 --- a/documentation/solid-2.0/MIGRATION.md +++ b/documentation/solid-2.0/MIGRATION.md @@ -1001,37 +1001,15 @@ The key difference: writes go through `setState`, which makes them visible to th import { from } from "solid-js"; const signal = from(observable$); -// 2.0 — async iterables are first-class in computations. Return the iterable -// itself: disposing the owner calls its return() and unsubscribes at once. -const value = createMemo(() => observable$); -``` - -Solid calls the returned iterator's `return()` when the owner is disposed and when a re-run replaces the stream. When the memo needs logic around the source (mapping, filtering, picking the source from a signal), an `async function*` is the natural fit, but don't hand it to Solid as-is. While the generator waits at an `await`, its `return()` is queued, so the source stays subscribed until it produces another value. Close the source yourself in `return()`: - -```js -const value = createMemo(() => { - // Read signals here, before any await, so they are tracked. - const source = feedFor(id())[Symbol.asyncIterator](); - const scale = factor(); - const gen = (async function* () { - for (let r; !(r = await source.next()).done; ) { - if (r.value.visible) yield r.value.amount * scale; - } - })(); - return { - [Symbol.asyncIterator]: () => ({ - next: () => gen.next(), - // Close the source at once; the generator's own return() waits for its await. - return: () => { - source.return?.(); - return gen.return(); - } - }) - }; +// 2.0 — async iterables are first-class in computations +const value = createMemo(async function* () { + for await (const val of observable$) { + yield val; + } }); ``` -A source that isn't an async iterable (a callback subscription, an Observable) needs an adapter whose `return()` unsubscribes synchronously (see #4182). +Cleanup is not immediate. When the owner is disposed, or a re-run replaces the stream, Solid calls the generator's `return()`, but a generator waiting at an `await` can't finish until that `await` settles. The source stays subscribed until it produces another value or ends. If the memo only forwards values, return the iterable itself (`createMemo(() => observable$)`): Solid closes it right away. A source that isn't an async iterable (a callback subscription, an Observable) needs an adapter (see #4182, which also tracks prompt cleanup for generators). **Solid → External (`observable`):** There's no drop-in replacement. `observable()` produced a standard Observable that external libraries could subscribe to. In 2.0, use `createEffect` to push signal changes to an external subscriber: From 80bb89063b200a8fd81b619e399db0bc894c45d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 09:37:38 +0530 Subject: [PATCH 4/4] docs: note that refs run before attachment, and that stores ignore later mutation of the objects passed in --- documentation/solid-2.0/04-stores.md | 2 ++ documentation/solid-2.0/07-dom.md | 2 ++ packages/solid/CHEATSHEET.md | 2 ++ 3 files changed, 6 insertions(+) diff --git a/documentation/solid-2.0/04-stores.md b/documentation/solid-2.0/04-stores.md index 19b859ad98..09effefcec 100644 --- a/documentation/solid-2.0/04-stores.md +++ b/documentation/solid-2.0/04-stores.md @@ -167,6 +167,8 @@ setStore(s => { This pairs naturally with `createProjection`, where returning a value from the derive function uses reconciliation automatically (keyed by `options.key`, default `"id"`). +Don't keep mutating an object after handing it to a store. A later in-place change to that object isn't observed, and assigning or reconciling the same object again doesn't pick it up either, because the store sees the same reference. Pass a fresh object instead (a new response, or a copy such as `{ ...next }`). This mostly affects test fixtures and caches that reuse one response object. + ### Shallow stores (`shallow: true`) By default stores track at property level all the way down — that is the model, and it is the right choice for almost all state. `shallow: true` is a **specialized performance opt-in** for one identifiable workload shape, not a general recommendation: reach for it when profiling shows ingestion cost on data whose records change wholesale. diff --git a/documentation/solid-2.0/07-dom.md b/documentation/solid-2.0/07-dom.md index 34d471747f..836f198a7b 100644 --- a/documentation/solid-2.0/07-dom.md +++ b/documentation/solid-2.0/07-dom.md @@ -93,6 +93,8 @@ let el;
(el = node), autofocus]} />; // assigned ``` +A ref runs before the element is inserted into the page. At that point `el.isConnected` is `false`, and `el.ownerDocument` is the inert document of the template the element was cloned from, so `el.ownerDocument.defaultView` is `null`. Don't reach the window or document through the element inside a ref: use the global `window`/`document`, or do work that needs the attached element (measuring, reading computed styles) in `onSettled`. + #### Two-phase directive factories (owned setup, unowned application) The recommended directive pattern is **two-phase**, similar in spirit to “split effects” (compute phase vs apply phase): diff --git a/packages/solid/CHEATSHEET.md b/packages/solid/CHEATSHEET.md index 9a6d98bcfc..5bbfc7c0c8 100644 --- a/packages/solid/CHEATSHEET.md +++ b/packages/solid/CHEATSHEET.md @@ -447,6 +447,8 @@ hydrate(() => , document.getElementById("root")!);