Skip to content

@solidjs/signals@2.0.0-rc.22

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 15 Nov 20:33
· 59 commits to main since this release
221b5f9

Patch Changes

  • 5755ddd: A pass that asks and reads ahead runs on the screen as a sync pass does (S-51 re-ruled 2026-11-12; F-2, F-6, D-25, #4778). An async memo that reads a held value beside latest(), isPending() or a guess of it now answers the screen's question for the display — 0/true, 0/1 beside the screen's id — and the landed world's question for the frame, so the write never shows beside another question's answer. The 2026-11-07 producer clause ("a question is asked only of the truth") is withdrawn: under it isPending(id) read true beside an async label computed from false, and a wrapped async reader (memo(() => asyncMemo())) behaved differently from an inline one.

    The hold waits for the answer to its own question. The request the frame's pass sent is the hold's question; the hold waits on it while a reader on screen does, whatever the screen's pass came to (before, a faster screen answer could land the write ahead of its data). A second write while that request is up supersedes it: the reader asks the new question at once instead of at the landing, and never ends on the first write's answer (on next, async(source(), memo(latest(source))) did). A window extracted into a memo reads as inline for an async reader over it, in either read order.

    A derived store whose derive reads such a memo shows what the memo form shows (D-30).

  • ddf8281: The attribution engine no longer keeps disposed scopes, async sources and loading boundaries alive. The costs() and feedback() rows were stored in maps keyed by the node, and the rerun history reached the written signal and the writing effect through its records, so a disposed component stayed reachable, with its owner and DOM, until the engine was reset or the record left the history. Rows are now looked up through a WeakMap, the history holds those nodes through a WeakRef, and costs() folds the rows of collected scopes into one row per name and kind, so a component that mounts again adds no row. A scope's own row is gone from costs() once the scope is collected; its totals are in the shared row.

  • cf32a50: A projection whose function returns another store as is now reads that store's values after a switch made while a guess is out (#4762). The outer store's own nodes still carried values from an earlier pass in the guess's lane. Once the store was chained, those values covered the inner store, so length and a row of the old list were served beside the new list's rows. A chained store's nodes now read through to the inner store. Guesses on an optimistic store are still served first.

  • ce7b4bb: fix(signals): a guess standing in for another action's held refresh keeps that hold waiting for the refetch (#4786)

  • aaf528d: Three lane-engine arms that no suite, pin or generator case has ever executed are removed: heldBy (the "frame parked by one pass alone" exemption at the seam, #4439 — it never returned true, not even in the suite of the commit that introduced it), the bornHeldOpen tail of attachWaits (#4574 — the mount stamp of S-46/S-58 seats a born-held mount before anything inserts it, and lane work never joins a hold, D-37, so the walk never found a node), and under (nested lanes cannot exist: a lane is never the flush's transaction, D-37, so no guess is ever parented to one). The lane link merge (linkLanes, A15 for lanes) stays: it is reachable — four in-flight optimistic actions read as two pairs, then a later reader across the pairs — and now has a pin. No behavior change; −260 B brotli / −900 B minified on the isPending/latest chunk.

  • d476295: Remove the "transaction only a latest() read opened" arm (latestOpened / openedByLatest and the stagedScreen test on it). latest() has opened no transaction since #4393, so the flag was never set and the arm could never run; a coverage inventory across every suite and 150,000 generator cases confirmed it (reader-seat-rebuild.md §9.5). No behavior change; −63 B minified on the isPending/latest chunk.

  • f6b6737: Add DEV.setSignal so development tooling can write signal values and store property slots (#4679).

  • 2789ca9: A display-ahead gate is the same mainline mount with any number of memos between the probe and the branch (S-58, D-25). A hand-written gate — one memo that probes and mounts, or a truthiness memo with a branch memo reading it — now keeps its fallback through the flight and attaches at the landing, as <Show> and the compiled ternary do: a probe's held reader whose frame holds a born-held mount is no longer published with its hole empty when the probe re-derives to the same value at the seam, and a mount under an open action counts as still waiting. The plain read under an errored boundary inside an action (S-53′) attaches at the landing with it.

  • db3d13d: A mount a display-ahead gate opens keeps its seat whatever its own first pass reads ahead (S-58′, D-25). A branch leaf that reads latest(x) beside a plain x() in one grouped effect, under a hand-written gate, attached early with the committed value beside its own ahead read; it is now born held like a plain-only leaf, as it already was under <Show> and with one effect per attribute. Neither hop count nor grouping changes the screen. The same mount under a guess lane (the write inside an action on an optimistic signal) was served the committed value of the lane's re-asked node by the lane's stale-reader arm and revealed with it at the action's landing, through <Show> as through a hand-written gate; it is now born held on the lane's flight too (S-58″, S-58‴).

  • bc423eb: fix(signals): a display-ahead <Show> (or ternary) keeps its fallback while the write that opened it is held, whatever the child's shape: element, fragment, text beside a hole, function child, or plain text (S-46, S-58, #4806)

  • 58e47eb: A createProjection or derived createStore whose derive writes another store's proxy into its draft (d.selected = rows.find(...)) now stays live. It used to keep a detached copy that later writes to the source store never reached. The return form already chained to the source, and the draft-write form now does too (#4845).

  • b7bb676: fix(signals): an action's truth landing under its own guess on a carrier another add shares stays beneath the guess until the shared hold commits, so the same card no longer shows twice (#4874)

  • 4ed5b48: fix(signals): a failed async memo under <Errored> is no longer re-asked on every flush when a <Loading> beside it reads it through latest(): behind a shown error fallback, a loading boundary whose reader answered with that error keeps its fallback instead of revealing (#4870, regression from #4634)

  • 8858189: The flush record lists what each drain changed on screen; the attribution hooks shrink around it

    • FlushEvent.changes: one entry per signal, memo or store property whose shown value the drain changed — by a commit, a held transition's landing, or an optimistic value appearing or lifting — with nodeId, nodeName, key (store properties) and origin (the action step, effect, interaction or navigation that staged the value; a held write keeps its origin, so a landing names its action). First values and effects are not listed. New types FlushChange and FlushLive: the record's live is now { changes: [{ node, prev?, value? }] }, index for index with event.changes, the values only for a listener that subscribed with { bodies: true }.
    • Visibility is judged once, by the engine. Core reports where a value was staged (AttributionHooks.staged, from queuePendingNode and list), never where it became visible; the engine compares what each staged node showed before and after the drain. OPTIMISTIC_REVERTED and the fallback record's display instant are judged from that comparison. Removed: the optimisticReverted hook, the engine's per-transaction copy of staged fallbacks (stagedFallbacks and its rebasing on settle/merge), and the schedule() a mounting boundary made so the engine got a drain end.
    • Hook inputs reduced to what core knows (internal AttributionHooks): recomputeEnd(el, create, changed, phase, held) replaces the optimistic/transition pair with the phase the engine derived from them, and absorbs derivedChanged; asyncLanded(el, value, changed) replaces asyncStart + asyncEnd(direct) — an async landing through setSignal is reported once, not first as a write (the engine no longer snapshots the node to tell whether it committed); storeWrite(target, old, written, keys) replaces storeReplaced, and the immutable-update census moves from the store into the engine; boundaryFallback takes the boundary's output instead of the transaction its swap is staged under.
  • 5208944: Prevent an untracked staged-store read beside latest() from throwing and halting reactivity when the store has no reactive node. A pass whose verdict posture is only inherited is now served the hold's own write from a node-less store key, as it is from a signal or a key with a node, so the landing frame no longer tears.

  • 6751472: fix(verdict): a reader of a guess in the guess's own lane is not marked with the truth's staged error, so a failure under an open hold no longer shows early beside readers that keep the guess (#4751)

  • 5ecd72f: A guess an action writes after reading another action's live guess, in its body or its setter, now entangles the two actions: neither guess reverts while the other action is out (#4788).

  • 4de85c2: fix(store): a guess computed from another action's live guess through a replaced child, an in check or key enumeration derives from it, as a direct read does (#4812)

  • c927463: fix(web): useHead tags of content behind a boundary's fallback are withdrawn while the fallback shows (an <Errored> keeps its content alive for reset(), so the error page kept the failed page's title) and come back when the content reveals (#4873)

  • 4ba1731: A controlled input no longer loses typed text when its signal write is held (an async derivation of it is pending) and another binding in the same template re-runs, such as a sibling isPending class flipping: a stateful DOM prop (value, checked, …) in a grouped effect or a spread skips an unchanged write while the effect is a stale reader of an open hold, and the hold's landing writes the bound value (#4872). Outside a hold every run still writes (#2016, #4186).

  • 803d269: feat(web): dev warns INVALID_HTML_NESTING when hydration claims an element the browser's HTML parser moved out of its parent (invalid nesting across a component boundary: <div> in <p>, <a> in <a>, <tr> straight in <table>); the key-miss warning for a <form> names a form nested in a form (#4875)

  • 6b7474b: An async memo that reads isPending(x) no longer shows its verdict beside an isPending(x) binding that disagrees with it, and nothing derived from the write it reads ahead of shows before that write does (#4677).

    • When the memo's lane landed in the same seam as the write it reads ahead of, the landing voided the binding's lane run, so the memo's pending: true showed while the binding stayed false. The lane now reveals whole (F-6), and the landing's re-derivation follows next round.
    • When the lane landed first, the write's landing re-derived the memo onto a new flight and held its own runs for it, but that flush's runs of effects deriving plainly from the landing still ran: details: Item 1 showed beside id: 0. Those runs now wait with the landing's held runs (F-1, F-7); what derives from a verdict reader still shows now (F-6, D-25).
  • dc33de6: At a hold's landing, a memo that read the screen while the hold was up (for example [latest(x), x()] or isPending beside a held write) no longer shows its stale value for one frame. The runs of its readers that were held with the landing now re-run on its re-derived value.

  • 9e02d02: A derivation born in a guess lane re-derives on the truth when the lane is corrected, so a keyed row that replaces a guessed row in the same commit renders (#4861), and a Loading region remounted while the action is out shows its content instead of nothing (#4863).

  • 54a5d1b: fix(signals): a render effect born in a lane pass is not marked uninitialized, so the flush no longer loops after a re-ask lands with several guesses out (#4871, regression from #4865)

  • f00ce6f: Rows that a held optimistic lane built keep their text when a correction ends the lane: a render effect born in that frame now runs on the committed values instead of losing its first run with the lane's parked runs (#4831).

  • da402ca: fix(lanes): a corrected guess lane voids a staged error with the rest of its staging, so a refresh's failure reaches Errored (#4748)

  • e90c209: A guess lane no longer shows an error that another action still holds. <Errored> waits for the hold, as it does when nothing reads a guess, instead of flipping early and then recovering over the previous question's answer (#4841).

  • 14b7cc6: fix(signals): a render effect in one lane reads another lane's member that waits only on refresh() re-asks as the screen, so its own guess shows (#4889)

  • dba2aad: A guess lane is no longer held by a member that waits only on another action's refresh(): a memo over the shown guess and a fresh answer (a heading beside a list being re-asked) now shows the guess over the answer instead of its stale committed value (#4834).

  • e20b4e7: Changing a keyed <For> selection while a createOptimistic guess its rows read is out no longer loops forever: a binding that reads a guess lane's member while that member's own fetch is up re-derives when the fetch lands, not again at every reveal of the lane (#4840).

  • 2801faf: A guess lane's pass that is served the committed value of a flight still in the air keeps the node's pending, so a <Loading on> fallback waiting on that flight stays up instead of showing the previous key's rows (#4862).

  • b4647d5: Lane work that was served a flight's committed value and then reads a write of the same flush waits on the flight before the flush has a transaction too, so a <Loading on> over a memo reading a guess shows its fallback for a new key instead of the old answer under it (#4859).

  • 79b6541: The lane engine is split along the guess boundary: lanes.ts keeps the sub-hold core every lane needs (the lane record, link / blocked / reveal / landing, the lane read and a lane pass's tail — what isPending() and latest() install for their verdict lanes), and the new guesses.ts carries what only a written guess reaches (createOptimistic / createOptimisticStore: optimisticWrite, the seam's applyGuesses, take, supersede, laneCorrections, laneOutcome, a write landing on a guess, a revert landing's give-back), installed behind hooks when imported. Pay-for-use (#2883): an app using isPending/latest without optimism no longer ships the guess layer (−531 B brotli on + isPending/latest, −454 to −533 B on the live pages); an app with both pays the indirection (+15 B brotli). No rule or screen changes.

  • 45498e3: An error seen through latest() now publishes with its reader, as a value would. Inside an open action, an <Errored> around latest(m), or around a memo of it, shows m's held error ahead, and later errors and the recovery reach that fallback the same way. Plain readers still wait for the reveal (D-41).

  • e0f31bc: mapArray (and <For>) copies its source list once at the start of a pass and diffs that copy, instead of reading the list throughout the pass and copying it at the end. A deep store list answers every slot read through a proxy trap, so each slot went through the proxy three or four times per pass and now goes through once. Removing 100 scattered rows from a 1,000-row store list goes from about 0.82 ms to about 0.72 ms, and bundles with mapArray are 23 bytes smaller minified. A plain array does the same single copy as before, earlier in the pass.

  • 1bdd973: mapArray and repeat run their update pass in a module-level function instead of an arrow function created on every pass. V8 keeps optimized code for a closure only while an instance of it is alive, so each major GC discarded the list update's optimized code and the next passes ran in the baseline tier. Appending one row to a 500-row list right after a major GC goes from about 26 µs to about 4 µs. No behavior change.

  • 0895916: A memo over latest() or isPending() that only the screen reads now shows what the same reads inline show (#4776). Once a hold's own work reads it, such as a branch the hold opened or an async derivation, it becomes that hold's work, so the async asks only of the truth (#4778).

  • edeb5db: fix(store): a store setter writing a nested key inside another store that is still loading its first answer no longer throws NotReadyError. The nested write is dropped, as a write to a loading store is replaced by its first answer, and the outer store keeps following the inner one. Dev warns with STORE_WRITE_THROUGH_LOADING (#4808).

  • a01fd92: onFlush callbacks now each run in their own flush. Before, several callbacks queued in one frame batched into a single flush, so async work one of them started held the others' writes with it. Each callback is now its own frame (D-4): it commits on its own unless what it changes derives from held work.

  • 3dd86e8: optimistic stores: when an add's answer lands while another action's reorder guess is out, the reorder's re-base drops the placeholder it carried from the add instead of showing it beside the new row; and a list reader of a key the reorder only borrowed from the add stays subscribed to the reorder, so its end reaches a <For> after the add is declined (#4897).

  • d00f055: An optimistic array guess no longer keeps serving another action's expired guess at indices it did not itself write (#4784). A guess's frame is composed on the view, so it holds the other guess's values at keys outside its own record; an arrangement read now falls through on those carried-over keys — to the lending carrier while it lives, to the truth once it dissolves — instead of serving the frame's stale copy. Writes that change no keys and no length already belonged to the changed indices (#4724); the read side now matches when the lending guess expires while a disjoint guess is still out.

  • 38b6f85: fix(signals): two optimistic guesses that change an array's keys or length hold the list together again: the #4784 fall-through to a lending guess applies only to index writes that change neither (D-33)

  • 4fe7532: An optimistic guess corrected by an outside re-ask now stays the value for effects and untracked reads until its action's own refresh lands (#4690). When a refresh() from outside the action landed under a pending guess (createOptimistic, or a createOptimisticStore keyed, unkeyed or object-shaped), the action's hold stopped waiting on the guessed source's next flight. Where nothing rendered the source, the hold landed with the action's write, and effects and untracked reads showed the old truth until the action's refresh() answered, while rendered readers (a render effect, a text node under <Loading>, an effect beside one) kept the guess. Rendered output never flickered; effect-only readers disagreed with it. A shown guess that was corrected now keeps its hold open while its own source is in flight, as an uncorrected guess does.

  • b08afe5: A row moved into a keyed optimistic store that had just emptied no longer reads as undefined once every answer has landed (#4904). A reader of an index made while the guess was out (rows[0]) was born holding the emptied answer staged beneath the guess. The later landing that brought the row back only recorded its value on the leaf, since the guess covered the key, so the hold's commit put the older, empty answer back when the guess ended. A covered leaf's staging now takes the newer truth too.

  • eafe671: A row one store's guess moves into another store as it is (that store's proxy) keeps its proxy when the second store's re-ask lands with the same key (#4902). The landing used to make a fresh row there, so when the move's action ended the row showed the server's values and a guess still out on it from the first store (a toggle) disappeared until its own action ended. The lent row's target now adopts the server object, as a row a guess added does, and the toggle's guess shows on through it until its action ends.

  • ccb438d: fix(signals): a write is not held by an action whose hold waits only on its refresh() (#4828)

    A memo that an action's hold had pending only on that action's refresh() pulled a later, unrelated write into the hold (switching a filter between two optimistic stores). The action's guess keeps its hold open until the refresh answers, so the switch, the other list's answer and the switch back all waited for it, and the refresh's landing then showed the other store's rows. A re-ask holds nothing new: the memo now leaves the hold and its next pass is the write's own frame.

  • 78eaf88: createOptimisticStore: a setter that returns a new array is written as the keyed diff of what it changed, so it reads the same at every frame as the equivalent field write (#4737, #4739). While guesses are out, a landing keeps every keyed row's proxy, so a row whose earlier guess settled can be guessed again and shows (#4738).

  • cd4de28: Two guesses on one optimistic store around another action's refresh(): a render effect waiting on that refresh (through a projection) now shows a new guess with the other readers of its slot (#4824), and a second guess on a slot no longer disappears when the first guess's action ends while the second is still out (#4825).

  • e76279f: Dev now warns OPTIMISTIC_WRITE_WHILE_LOADING when an optimistic setter runs while its source, which has answered before, is loading a new question. The setter composes on the previous answer, and the new answer replaces the guess (#4795).

  • ab3fc44: A computation now runs after every computation that owns it, not only its direct owner. A binding nested under a <Show> branch (or the ternary it lowers to) no longer runs before the branch closes once the condition starts reading a deeper memo, which threw "Stale read from ." and halted updates (#4848).

  • 8b7254a: fix: a memo's two plain reads of a held value agree when its first latest()/isPending() of it comes between them (#4777)

  • 46a72c0: A question is asked only of the truth (F-2's producer clause, F-6's limit), on the M1 lane value model. An async derivation that reads a held value plainly beside latest()/isPending() is the hold's work: it asks once, of the truth, never of the value the screen keeps (S-51, #4677). isPending stays true for a landed answer the lane still holds on a sibling's flight (S-54, #4685). A held recovery to the last good value is a change of answer and latest shows it ahead, so an errored boundary recovers while the action is open (S-53, #4403). A guess written to an optimistic memo before its first answer shows and the first answer judges it (S-56; the memo-form OPTIMISTIC_WRITE_BEFORE_LOAD guard is gone, the store form keeps it). [isPending(x), x()] shows pending beside the kept value in every shape, a sync-ending action included (S-52, #4349). A plain write whose derivation became a guess's lane work holds on that flight and reveals with the lane (S-57). The branch a display-ahead <Show> opens is born held: the fallback stays through the flight and the branch attaches at the landing, for <Show> and the compiled ternary alike (S-46's plain row). latest() of a displayed optimistic guess keeps the guess over a correction until the commit, for a signal and a superseded keyed row (#4689).

    Lane values now live on the node's two slots with the cover recorded explicitly (M1): the separate lane slot, display() and laneValueOf are gone.

  • abbb806: fix(store): presence and keys of a slot a lane pass derived read the same frame as its value, so mapArray over a keyed projection no longer maps an empty slot after a declined removal (#4821)

  • 6681c1b: A projection read by another projection's derive is brought up to date before it is read, and its reader is ordered after it. A write that dirtied N rows no longer re-ran each downstream reader up to N times (#4782).

  • 640841d: A mapArray (or <For>) over a createProjection of an optimistic list now follows a reorder guess. The projection's new arrangement in the guess's lane was taken as a guess landing on itself, so its readers never heard of it and the list kept the old order, even after the refetch landed (#4843).

  • 57a7f42: A <Loading on> re-armed by a guess catches a pending its own content read runs into, so the guess's lane reveals with the fallback instead of waiting behind it with the previous selection's values (#4856).

  • a8b8b67: fix(signals): a guessed store re-asked through its sources is a re-ask, not a new question held behind the guess's action (#4801)

  • c3e48a5: A render effect a guess lane runs reads a member that waits only on refresh() re-asks as it shows, so rows the lane rebuilds while a sibling refresh is out bind with the guess (#4864).

  • d739d6f: fix(signals): a refresh() over another action's held write is a re-ask, so a later guess shows to the readers waiting on it (#4887, #4888)

  • 9c917ea: A ref callback that reads a sync memo on a row mounted while an async memo is in flight no longer leaves that update held forever (#4767). Refs run in the attribute effect's commit half with no owner; a memo born into the hold has only a staged value, and an ownerless read of it threw NotReadyError (A19 exception 1), which errored the effect mid-hold so the transaction never landed. runRefCallback now marks the read, and serve returns the staged value only when the ref's creating pass was born into that memo's hold (F-7); production ref() always goes through runRefCallback so the mark is present outside dev too.

  • 7230c08: A ref callback (ref={el => …}) now runs the way a render effect's callback runs at creation (#4761). It is untracked and runs in the context of the pass that creates the element, so its reads see what that pass is building. Before, it ran with no owner, so its reads were outside reads that saw the screen before the pass committed. A non-keyed <Show>/<Match> branch that an optimistic write had hidden could not come back: its ref read the narrowed accessor, got the old falsy condition, and threw a stale read that halted the app.

    A ref still owns nothing. In dev, creating a memo, effect or root inside it throws PRIMITIVE_IN_EFFECT_CALLBACK, and onCleanup inside it throws CLEANUP_IN_FORBIDDEN_SCOPE. Set those up in the factory that returns the ref, which runs in the component body. Writes, refresh, action calls and onFlush stay allowed in a ref, as in an effect callback.

  • 9d4f077: A second optimistic guess whose consequence is still pending no longer reaches a reader re-run by an unrelated write: it is shown with its lane's next reveal (#4775).

  • fab8bd9: fix(signals): a second guess on a keyed optimistic row shows after a refetch corrected the first one, instead of being dropped as no change (#4869)

  • fb672ca: IMMUTABLE_UPDATE_IN_STORE no longer reports wholesale replacements on a shallow store, or of a markRaw / unwrappable leaf (#4686). Those values are invalidated as a unit, so leaf draft assignment does not narrow who re-runs — the diagnostic's advice does not apply.

  • e76279f: A guess shown while its source loads a new question stays on screen until its action's hold commits, instead of giving way to the new question's answer while the action is still out (#4795).

  • b050983: mapArray's small-move pass (a swap, a rotate or a few displaced rows in a plain <For>) records each aligned run once and checks displaced rows for duplicates with a native scan of the window, instead of building a Set and probing every aligned row. A swap on 1,000 rows goes from about 10 µs to about 4.5 µs, and bundles with mapArray are 97 bytes smaller minified. No behavior change.

  • 9de15ba: fix(signals): a staged store error read from a bare owner (a <For> row update) no longer links it as a computation, so <Errored> gets only the fetch's error (#4805)

  • d06e7a7: A bundle that uses stores without importing deep() no longer ships the store's deep-tracking bookkeeping: the write and reconcile paths check a module flag that only deep() sets, so a bundler drops the guarded code.

  • c981680: An optimistic store's guess is one node per setter per lane: the guess carrier, whose value is the setter's draft with the keys it changed recorded on it. Reads of a guessed key go through the carrier; a landing is judged against it over those keys. A rewritten or returned top-level array is one write per index that differs, as a returned object is one per key, and the list itself is the slot only when its keys or length changed. Two actions guessing into one list (a field edit and a whole-array rewrite alike) are two carriers in two lanes: a guess made while another action's refresh is in flight shows from its flush, and the refresh's failure is the list's answer and reaches Errored (#4724). The OPTIMISTIC_REVERTED finding names the guessed key.

  • b3e9fb8: A spread or a merge()/omit() consumer that reads the keys of a store now asks the store's handler for them directly instead of going through the proxy with Reflect.ownKeys. The engine validates every proxy ownKeys result against the proxy's target, and for a store that check cost about twice the trap itself. Reading the keys of a 12-key store this way goes from about 880 ns to about 280 ns, and re-running an element spread over a store record takes about 10% fewer instructions. The keys returned and the tracking are the same; any other proxy (a merge or omit view, a projection draft, a foreign proxy) is still read through Reflect.ownKeys. Bundles that include the merge/omit source walkers are 45 bytes larger minified.

  • 8a6de0f: Dev builds now warn with STRICT_READ_UNTRACKED (#1742) when an element's event handler expression reads reactive state, e.g. onClick={selected() ? a : b} or a props.onClick the parent passed a reactive expression. A handler is bound once, where its element is created, so such a read is not meant to be reactive; the warning says to read the value inside the handler function instead. A pending async read there throws PENDING_ASYNC_UNTRACKED_READ, as in a component body. The read still tracks, so dev re-mounts on a change exactly as production does.

    Production output is unchanged byte for byte. In dev DOM output both compilers wrap each handler expression that can read reactive state (anything but an identifier, an inline function or a literal), including the [handler, data] form's bound values on delegated events, in a new strictHandler(() => expr) helper from @solidjs/web. It reads expr through strictReadTracked, a dev-only strict-read window that keeps tracking (internal, from @solidjs/signals via solid-js). No production-compiled module imports either, so they tree-shake away.

  • bcef104: A plain write made while an optimistic guess is out now waits for the async it needs, even when the memo that needs it is the guess's lane work. Before, that memo got the old async result alongside the new value. For example, a filter switch showed "closed" with the open rows until its fetch answered, both through a keyed projection and under a <Loading on> boundary, which also dropped the guess from the screen. The screen now keeps what it showed, an armed <Loading> shows its fallback, and the switch lands with the fetch's answer.

  • eef34fd: Fix two cases where a conditional written as a ternary behaved differently from <Show>. At a component root, <>{c() ? a : b}</> now opens its branch at once under an action's optimistic guess, as <Show> does (#3835). An isPending ternary in an <Errored> fallback no longer keeps the first error's text after a reset when the source fails again.

  • ebd6283: Two gate shapes S-58 says must match <Show> now do (#4405, #4708 follow-up).

    • A zombie no longer retries an errored source on a later cycle's read. The old content of a remounted gate is a zombie until the hold lands. With one memo between an errored latest() reader and the branch, the zombie's retry threw a fresh error that re-derived the live gate, and the live gate's retry next round re-derived the zombie again: "Potential Infinite Loop Detected" in dev and a hang in prod. A zombie's fate is the seam's (L2), so it throws the error it has.
    • A mount a probing pass opened (S-46) now also covers the passes its first pass creates. An <Errored> around the branch's content made the content's effect a plain leaf, so it attached with its hole empty during the flight (F-2) through a hand-written gate.