Patch Changes
-
3d8a8a9: Point JSDoc
@descriptionlinks at the 2.0 reference docs (v2.solidjs.com), documentisPending, point the console footer, diagnostics guide and cheatsheet links at thenextbranch, and replace the staleonMountadvice in the server-side client-only API error. -
a9b9f9e: Route synchronous throws from the async iterator / thenable protocol (a later
next(), a step'sthen, or deferred iterator setup) to the owning memo's error handling instead of escaping as unhandled rejections, and makeStatusErrormessage formatting best-effort so it never replaces the original error (#4112). -
a7fe050: A Loading boundary mounted by a pass that joined a hold appears at that hold's commit, its fallback never shown (D-13, U-3). Before, the boundary's content was caught as a fresh mount and the fallback published at once (or, for a
<Show>that opens when a flight lands, flashed at the landing before the content). A first pass born into the flush's hold is now the hold's, not the boundary's: the content reads with the hold and the whole mount appears at its commit. A mount made by a verdict reader (latestin a<Show>) is still mainline and shows the fallback while the hold is up. -
52008d9: An effect or memo that catches the
NotReadyErrorfrom reading an async derived store (createStore(async fn, seed),createProjection) now re-runs when the store's flight lands, as it does for an async memo (#4070). -
0b6765f: A signal write from an
onCleanupthat a transition's landing runs no longer leaves the landed transaction as the next flush's: later transitions commit again instead of parking forever (#4089). -
a2743fa: Allow writes in cleanups during teardown (#4165). Removing a keyed
<For>row ran the row's cleanups inside the list's pass, so a setter in anonCleanuporonSettledcleanup was rejected in development as a write in an owned scope, which halted reactivity. A cleanup is teardown, not part of the computation disposing it, so the dev-only owned-scope write check now stands down while cleanups run. Cleanups keep their owner and context, and production builds are unchanged. -
b8617ec:
createDeferred(fn, options?): an async memo that may lag, but never leads. Before its first answer it is an ordinary async memo (readers suspend to<Loading>, orloadingValueshows). After it, a refetch holds nothing: readers keep the previous answer until the new one lands, the rest of the screen moves on, andisPendingreports the refetch on the node and on everything derived from it, store keys included. A refetch that read a held value lands with that hold. A rejection errors like any memo.refresh(d)is a quiet re-ask, andawait refresh(d)anduntilwait for the landing. It iscreateMemoon the server and hydrates like one.The rules: F-1 and F-2 are amended for declared lag, and D-40 / S-41 are new. The
affects()mark core moved to its own module (affects-marks.ts), whichcreateDeferredshares. A mark on the source of a derived store now covers that store's keys. -
3643c5e: A
createRootdispose function no longer keeps the root'sinitfunction (and everything it closes over) alive while it is held (#4095). -
4a99c84: Diagnostics carry a source location.
NodeOptions.locandStoreOptions.loc(file:line:col, root-relative — written by the compiler'ssourceNameslocationskind, not by hand) land in a_locslot beside_nameon the observe/dev node literals; store property nodes carry their store's.DiagnosticEvent.locis the innermost located node on the subject's chain (its ownloc, else the first up the owners — a signal through its registering owner), found inemitDiagnosticthe wayownerPathis; a host event that sets it keeps it.reportDiagnosticprints it as anatline after theinline —at src/App.tsx:18:3, or in a browserat http://localhost:5173/src/App.tsx:18:3, which the devtools link to the Sources panel.DEV.guideUrl(code)now returns the code's page on the docs site (https://v2.solidjs.com/reference/solid-js/diagnostics/<slug>). -
2172a37: A custom
equalsthat reads an unresolved async source (a getter over a pending memo) no longer leaves the memo stuck on aNotReadyErrornothing retries. A comparator that throwsNotReadyErrorhas not shown the values equal, so the value is published as a change; its readers dereference it in their own tracked context, suspend under their<Loading>boundary and recover when the async work lands (#4111). Other comparator throws are still errors of the node, and a signal setter's comparator throw now takes the same path (its error boundary contains it) instead of throwing at the caller. -
1bcee91: Server components hydrate their client slot occurrences on demand. The adoption pass mounts none of them; an occurrence mounts when an event lands in it (outermost first, before the event reaches a handler), before any signal or store write applies (every remaining occurrence, against the server's state), or in idle slices after the load event. Hydration stays in progress until the last one has mounted. The write side is an internal pre-write hook in
@solidjs/signals, tree-shaken from apps without server components. -
47a217f: Frames: a signal or store write during deferred hydration mounts only the slot occurrences the server recorded as its readers, then applies, instead of mounting every pending occurrence first. The server names plain signals (
<owner id>~<n>) and plain stores (by their root) in server-component renders and emitssc:depsrecords per frame; writes it cannot vouch for (unnamed, derived, optimistic, projection or wide sources) still mount everything first. Apps without server components keep their sizes and code paths. -
61c4436: An async memo created inside a fresh Loading boundary over a held value no longer lands its first answer before the hold commits (D-12, U-1). The boundary still catches it and shows its fallback, and the hold still does not wait for it: its answer is staged with the hold and appears no earlier than the hold's commit. A boundary re-armed by
onkeeps owning what it collects outright. -
a39acdd:
GRAPH_GROWTHno longer reports long-lived state that the page just left also read. What outlived a navigation was the set of nodes reachable as that one navigation opened, and the walk reaches a module-level source (solid-refresh's per-component registry signal in dev, a global store) only through a page that reads it. So arriving at a route from pages sharing more and more of its components, or of its global state, read as growth with nothing leaked. The nodes met as navigations open now go into one set kept for the session, so such a source counts the same at every settle once met, and only what keeps accumulating climbs. Leaked roots, ownerless effects and per-visit subscriptions still report as before. Fixes #4101. -
667dcff: A write to a store key that nothing reads no longer waits for an unrelated action that is holding a sibling key on the same record (#4146). Before this fix, if a transaction held the record's container (an action wrote
card.versionand was still pending), a later setter outside that action writingcard.textput the new value into the held staging. Plain reads ofcard.textthen showed the old value until the action finished. With a live render effect readingcard.text, the same write showed up at the next flush, because the effect's leaf node gave the key its own staging. Node existence is now unobservable here too (R1, #3706: a hold covers individual keys). The setter creates the key's leaf before writing, from the committed and staged values, so the leaf starts out unstaged and held by no transaction. The write then stages on that leaf and becomes visible at the flush (A28). A leaf on a held container is kept alive until the hold lands, so disposing a reader cannot pull the key back into the hold. A write to a key the hold itself changed still joins the hold (A34). Array elements follow the same per-key rule, so writing an index the hold left unchanged also shows at the flush. An added key, or an index appended to a held array, now behaves the same whether or not something reads it: its value shows at the flush, while the record's key list (or the array's length) still waits for the hold to land. -
0bda987: An error is an answer: it ends the wait like a value, and it shows when a value would (D-41). A rejection or throw on a node a transaction holds is staged and published with the frame that carries it: until then the screen, an untracked read,
createEffect's error arm and<Errored>keep the previous frame,isPendingistrue,latestthrows the proposed error, andresolve/until/refreshreject at the landing. A held recovery keeps the shown error until its own commit, and a recovery equal to the last good value now reaches effects, held or not (it was swallowed by the equality gate). Replaces the separate staged-error publish path. From #3886's audit (GabbeV). -
fb6006e: A synchronous memo that throws in a flush that later holds now waits for its frame like a value (D-41, #4109). The error was staged only when the flush had already joined a hold, so a pass that ran before its async sibling went pending published the error at once: an untracked read (a click handler) threw while the screen still showed the committed value.
A later pass of the same node replaces a staged error: a value, a pending flight or a new error supersedes it, so the value is no longer thrown as an error (#4145) and the superseded error is no longer published at the commit.
-
ccd9948: Smaller floor bundles: the optional engines (lanes, verdicts, boundaries, stores, affects, external sources) install their scheduler hooks as module variables, so an app that never loads them no longer retains their paths in
recompute,readand the settle step; client-onlyrenderno longer retains hydration text adoption, and the delegated dispatcher drops dead guards. The JSX hello world goes from 10.35 KB to 9.75 KB brotli. -
1b2406d: A child under a non-keyed
<Show>no longer re-runs after its condition goes falsy inside an open action (#4137). When the branch is lane work — its row was added by an optimistic write — and a later pass of the same lane withdraws it, the Show parks the branch as a lane frame (#3698): it stays on screen until the lane's apply retires it. The condition's re-staging marked the parked child as the lane's work, and since the L2 rewrite (rc.14) a lane-dirty zombie ran at once, so the child read the Show's narrowed accessor after the selecting read went falsy ("Attempting to access a stale value from ") and, without an error boundary, halted the reactive system. A lane frame's member now follows mainline writes only: a lane dirty on a zombie whose parking owner is lane work is cancelled. A transaction's zombie a lane reveals through still runs for the lane (#3463). -
7093b07: A memo over
latest(source)now sees a held rejection at the same point it sees a held value (#4106). Such a memo is display-ahead (F-6): when a held flight onsourceresolves, the memo re-runs in the hold's verdict lane and its new value shows at once, while a sibling still holds the frame. A rejection did not reach it the same way.stageErrorwoke the memo but scheduled no flush, so the memo kept its old value until the reveal. After the memo did re-run, its lane pass errored, butlatest(memo)andmemo()kept serving the lane value from before the error, while the memo's effect had already received the error. NowstageErrorschedules the flush, and a read of an errored lane member that is not a written guess throws its error.latest(memo),memo()and the effect's error arm now showboombefore the reveal, just as they show2when the source resolves. -
dbc5d00:
SSR_BOUNDARY_WATERFALLnow judges a wait by when its work started, the same origin rule the client'sASYNC_WATERFALLuses. A nested route meets its levels one render pass at a time, but the router starts every matched level'slazy()import and preload together. Before, the first request to a cold server warned about a data waterfall that wasn't there. A wait whose work had all started before the previous wait landed is now left out of the count (data.parallelWaits).lazy()andlazyModule()stamp their imports. The start stamps move from the attribution engine to the core'sOBSERVE.attribution(markFlight,flightOrigin), so a router or cache can stamp what it hands out without loading the engine.attribution.markFlightwrites to the same place. -
6371b91: fix(signals): a lazy memo read only through a rebuilt child memo is not released mid-pass (#4142)
-
347f242: New dev check
SSR_LIVE_HOLE_LATCHED(#4023): a live markup hole that shipped its markers and then stopped updating, because re-running it would create reactive scopes (most often a compiler condition memo for{m.usage && …}), is now named on the console, located by its component, instead of going quiet. Behavior is unchanged; hoisting the property into a local keeps the hole live. -
f3973ea:
mapArraycommits a structural change without rebuilding full-length copies of its row bookkeeping: a pass stages only the changed window, lands it into the live arrays in place with the unchanged suffix shifted by a loop, and rebuilds only the mapped array that signals the change downstream. A pass that only removes rows is matched with two pointers and no key map, and the key-chain array is sized to the window rather than to the edit's position. One-item inserts and removals on long keyed lists, every-nth removals and sparse filters no longer allocate several list-sized arrays per edit; the strong-abort contract for a mapper that suspends is unchanged, kept by ordering (verify, create, then mutate) instead of by copying. -
c12c420: An outer
on-reset Loading boundary no longer waits for a flight its nestedon-reset boundary catches (D-14, U-4). A pending reader forwarded through both boundaries while neither was collecting was recorded in both, so when one tick re-armed both the outer kept its fallback until the inner's flight landed. A reader a nested collecting boundary holds is now that boundary's alone: the outer's content shows beside the inner's fallback as soon as the outer's own work lands. -
a2d5669: Fix an optimistic row that showed, vanished and reappeared when several actions guessed the same list in quick succession: a guess made over a slot another action still holds now entangles with that action, so it stays until its own action settles instead of reverting with the earlier one.
-
5c8ec96: Root owners now carry their lifecycle flags slot from creation.
disposeChildrenmarks death by writing_flagson every owner it tears down, and the root literal (everycreateRoot, everymapArrayrow and cell owner) did not declare the field, so the first write was a shape transition plus a properties-store allocation per root. On a 100×4 keyed table teardown that was 12 KB and most of disposal's time; uibench removeAll drops 64–70%, the sparse filters 33–61%, single-row removes 40–65%. -
4bc7969: fix(signals): a later mainline store setter that does not write a held key publishes on the mainline tick — a setter's written keys are its own set, so its notify visits only the keys it wrote (D-8, U-8, #3743)
-
d3cddff: Attribution:
createProjectionrecords the owner in scope ascreateStoredoes, so a projection's property nodes carry an owner path (#4035). TheGRAPH_GROWTHmessage and its skill section now name a cache that never evicts as a source of the same growth. -
7014aca: Preserve a function returned by a writable-memo updater during a held transition as data, without invoking it again.
-
c2513e3: A Loading boundary re-armed by
onowns the content under it that reads a hold another change made (D-14, D-16, U-9). Before, that content joined the hold, so no fallback showed and the boundary reported pending, and a held tree under a re-armed boundary could redraw forever. Now the fallback shows with the re-arming change and the content reveals at the hold's commit, which no longer waits for it. A re-arm whose own change joins that hold still shows no fallback (the frame waits on the same data), andLOADING_ON_OUTSIDE_HOLDis reported only for a flight the change itself started. -
32652bb: EFFECT_RELAY_TEAR no longer flags effects that measure layout. When the effect's apply half takes no argument (
createEffect(open, () => setFlipped(measure()))), a write that equals the compute output is not a copy, and its tears stay advisory however often they repeat. Effects whose apply half takes the value are judged as before, so boolean mirrors still warn. -
3f36f77: Server components: binding slots are removed. Client components in slots replace them: place a client component through a
Slot(<props.x />) where the server's markup needs client behavior, or a formactionfor a mutation. Removed with them: theBindingSlot,SlotOutputandSlotErrortypes, the lazy@solidjs/web/frames/bindentry and its"bind"tier, the_s:*position markers and the compiledssrClaimhole, the effect of theserverComponentscompiler option (@solidjs/compilerand@solidjs/babel-pluginstill accept it, so existing build configs keep working, and ignore it), and theBINDING_SLOT_POSITIONdiagnostic code (@solidjs/signals'DiagnosticCode,solid-js'sdiagnostics.json). Ordinary slots, theirslot:<id>ranges and records, and$keyare unchanged. -
dfb31ed: Derived stores no longer need a seed (#3194).
createStore(fn),createProjection(fn)andcreateOptimisticStore(fn)accept a derive that takes no draft and answers a complete value — sync, a promise, or an async iterable whose every yield is a complete snapshot. The root is a plain object (an array keeps its[]seed); passundefinedfor the seed to reach the options, e.g.createStore(fetchUser, undefined, { key: "email" }). Until the first answer lands the store is unreadable, as with any seed (A25);seedLoadingValueneeds a seed and is not offered on the seedless form.With the seedless form available, a seed is a complete
Tagain (#3258's requirement, reverted then because async stores that needed the options slot had to invent one). A{}seed on a return-form derive is now a type error: drop it. A mutating derive that builds its object from an empty seed needs{} as T. -
f420740: Deleting a store key now decrements the store's key count. The count went up on every added key but came down only in the overlay rebuild paths, so the natural keyed-selection shape — delete the previous key, set the new one — drifted it upward by one per write until a small object crossed the overlay threshold and paid a full rebuild of its backing on every commit after. A delete of a draft's own key (a plain clone's, or an overlay's own add) is counted immediately; an overlay's delete of a committed key is still recorded and counted at the rebuild.
-
745e807: Settle an immediately empty async iterable to
undefinedwhen a synchronous thenable resolves to it, matching direct and deferred empty-stream completion instead of exposing the iterable object. -
3fc5551:
until()andrefresh()no longer retain the optimistic lane engine in apps that don't use optimism (−4.8 KB minified). Anuntil()predicate reading a memo derived from an unconfirmed optimistic guess now waits for the guess to be confirmed or corrected, instead of seeing the value from before the guess. -
42c2568: A memo that reads a verdict memo (
isPending/latest) shows the verdict now, beside a held value it also reads (F-6, U-10, #4050). Before, it re-entered the hold with that value and the verdict waited for the landing: a Provider's children memo over a router shell[<Feedback/>, outlet]never showed delayed pending feedback during a held navigation. It now behaves as the same probe read inline, or a render effect reading both, already did. -
dc063a6: A
<Loading>that a verdict reader (<Show when={latest(x) > 0}>) mounts during a held action now holds a render effect nested in its content, not only a direct read: it shows its fallback beside the committed value until the action lands (#4166). -
71153bf: A write to a writable memo clears an error its derivation left behind (#4110). Clearing
STATUS_ERRORonly happened on a recompute or an async settle, and a direct write does neither, so a memo that had thrown kept throwing the stale error to every later reader: effects stayed on their error arm, error boundaries never recovered, and the written value sat committed underneath a node that still reported an error. A write equal to the last good value was dropped entirely by the equality gate. Writing to such a memo now recovers it, which is what "a write lands at once and never re-runsfnon its own" already promised.The error stays what readers see until the write commits, so a read between the write and its flush, or during an action that holds the write, still throws instead of returning the old value. A write after a pass staged an error under the same action now lands as a value, instead of reaching effects' error arm.