github solidjs/solid @solidjs/signals@2.0.0-rc.16

Patch Changes

  • 3d8a8a9: Point JSDoc @description links at the 2.0 reference docs (v2.solidjs.com), document isPending, point the console footer, diagnostics guide and cheatsheet links at the next branch, and replace the stale onMount advice in the server-side client-only API error.

  • a9b9f9e: Route synchronous throws from the async iterator / thenable protocol (a later next(), a step's then, or deferred iterator setup) to the owning memo's error handling instead of escaping as unhandled rejections, and make StatusError message 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 (latest in a <Show>) is still mainline and shows the fallback while the hold is up.

  • 52008d9: An effect or memo that catches the NotReadyError from 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 onCleanup that 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 an onCleanup or onSettled cleanup 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>, or loadingValue shows). After it, a refetch holds nothing: readers keep the previous answer until the new one lands, the rest of the screen moves on, and isPending reports 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, and await refresh(d) and until wait for the landing. It is createMemo on 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), which createDeferred shares. A mark on the source of a derived store now covers that store's keys.

  • 3643c5e: A createRoot dispose function no longer keeps the root's init function (and everything it closes over) alive while it is held (#4095).

  • 4a99c84: Diagnostics carry a source location. NodeOptions.loc and StoreOptions.loc (file:line:col, root-relative — written by the compiler's sourceNames locations kind, not by hand) land in a _loc slot beside _name on the observe/dev node literals; store property nodes carry their store's. DiagnosticEvent.loc is the innermost located node on the subject's chain (its own loc, else the first up the owners — a signal through its registering owner), found in emitDiagnostic the way ownerPath is; a host event that sets it keeps it. reportDiagnostic prints it as an at line after the in line — at src/App.tsx:18:3, or in a browser at 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 equals that reads an unresolved async source (a getter over a pending memo) no longer leaves the memo stuck on a NotReadyError nothing retries. A comparator that throws NotReadyError has 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 emits sc:deps records 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 on keeps owning what it collects outright.

  • a39acdd: GRAPH_GROWTH no 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.version and was still pending), a later setter outside that action writing card.text put the new value into the held staging. Plain reads of card.text then showed the old value until the action finished. With a live render effect reading card.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, isPending is true, latest throws the proposed error, and resolve/until/refresh reject 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, read and the settle step; client-only render no 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 on source resolves, 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. stageError woke 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, but latest(memo) and memo() kept serving the lane value from before the error, while the memo's effect had already received the error. Now stageError schedules 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 show boom before the reveal, just as they show 2 when the source resolves.

  • dbc5d00: SSR_BOUNDARY_WATERFALL now judges a wait by when its work started, the same origin rule the client's ASYNC_WATERFALL uses. A nested route meets its levels one render pass at a time, but the router starts every matched level's lazy() 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() and lazyModule() stamp their imports. The start stamps move from the attribution engine to the core's OBSERVE.attribution (markFlight, flightOrigin), so a router or cache can stamp what it hands out without loading the engine. attribution.markFlight writes 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: mapArray commits 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 nested on-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. disposeChildren marks death by writing _flags on every owner it tears down, and the root literal (every createRoot, every mapArray row 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: createProjection records the owner in scope as createStore does, so a projection's property nodes carry an owner path (#4035). The GRAPH_GROWTH message 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 on owns 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), and LOADING_ON_OUTSIDE_HOLD is 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 form action for a mutation. Removed with them: the BindingSlot, SlotOutput and SlotError types, the lazy @solidjs/web/frames/bind entry and its "bind" tier, the _s:* position markers and the compiled ssrClaim hole, the effect of the serverComponents compiler option (@solidjs/compiler and @solidjs/babel-plugin still accept it, so existing build configs keep working, and ignore it), and the BINDING_SLOT_POSITION diagnostic code (@solidjs/signals' DiagnosticCode, solid-js's diagnostics.json). Ordinary slots, their slot:<id> ranges and records, and $key are unchanged.

  • dfb31ed: Derived stores no longer need a seed (#3194). createStore(fn), createProjection(fn) and createOptimisticStore(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); pass undefined for 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); seedLoadingValue needs a seed and is not offered on the seedless form.

    With the seedless form available, a seed is a complete T again (#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 undefined when a synchronous thenable resolves to it, matching direct and deferred empty-stream completion instead of exposing the iterable object.

  • 3fc5551: until() and refresh() no longer retain the optimistic lane engine in apps that don't use optimism (−4.8 KB minified). An until() 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_ERROR only 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-runs fn on 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.

Don't miss a new solid release

NewReleases is sending notifications on new releases.