chore(deps): update dependency octane to ^0.8.0 - #8625
Merged
Merged
Conversation
chenjiahan
approved these changes
Oct 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
^0.6.0→^0.8.0Release Notes
octanejs/octane (octane)
v0.8.0Compare Source
Minor Changes
53db436:'use dom bindings'views can now drive whole islands without the renderer:count$.get(),count$.latest(fallback)) in projections, branch tests, lists and setup or blockconstdeclarations subscribe instead of raising an activation diagnostic. A pending read keeps the last DOM until its source changes.@try/@pending/@catchcompile into binding programs, select their arm like the renderer, adopt whichever arm the server rendered, and claim a boundary that is still streaming.useLayoutEffect(callback, [])anduseEffect(callback, [])run once after a view activates and clean up when it leaves.<Hydrate>whose only child is a prop-less, zero-argument binding view imported from a.tsrxmodule activates through that view's program, so its chunk no longer loads the renderer. Other islands keep the renderer activator.3e3749d: Letinteraction({ events })opt intopointermoveandpointercancel, so a deferred boundary woken by a press can replay whether that press moved, was released, or was cancelled by the browser before hydration finished.Both events only extend an interaction that another selected event already captured: they never start hydration or prefetch on their own, so selecting
pointermovedoes not make a boundary hydrate on hover. Octane registers their document listeners only after a boundary that selects one captures intent, so pages that do not opt in never listen for pointer movement. The defaultinteraction()events are unchanged.Capture no longer cancels the native default of a captured
pointeruporpointermove, matching how it already treatspointerdown.950ef0b: Development server renders now report the client work an islands-only shell actually renders. The Vite dev server renders ahydrate: 'islands'route with the newshellWitnessrender option and warns once per site when the shell, outside its independent<Hydrate>islands, renders an event handler or function form action, a ref, an effect or store-subscription hook, a controlledvalueorcheckedthe user can edit, or a live signal-handle binding. None of these would run, because the shell's modules never load in the browser.The report follows the render rather than the source, so it also catches what the build's source check cannot follow: components passed by reference, local aliases, handlers and refs passed through spreads, and elements a plain helper creates with
createElement. It covers only the branches, rows, and streamed boundaries a request reaches. The route keeps serving either way, and the production build check is unchanged.renderToString,renderToReadableStream, and the other server renderers acceptshellWitnessin development. Production renders ignore it at no cost, and the production compiler output is unchanged.dc4800b: Strong mode now requires state updaters and reducers to be pure(
OCTANE_STRONG_IMPURE_UPDATER). Octane evaluates queued updaters and reducerswhile their owner renders and can call them more than once: a transition
update is evaluated when it is staged and again when the transition renders,
and an urgent update made while a transition is held is rebased onto the held
value. A
useStateoruseLinkedStateupdater, auseReducerreducer, or auseOptimisticreducer that callsfetch, schedules a timer, microtask, orpromise callback, updates state, calls a state getter or Effect Event, touches
useRef.current, reads a browser global or reassigned module variable, orcalls
Date.now(),Math.random(),performance.now(), ornew Date()is acompile error. Inline functions, local and same-module declarations, and
synchronous helpers are followed. Mutating the state an updater or reducer
receives reports
OCTANE_STRONG_SNAPSHOT_MUTATION. Do the work in the eventhandler and pass the result in. Compatibility mode is unchanged.
dc4800b: Strong mode now rejects mutating a state value outside render(
OCTANE_STRONG_SNAPSHOT_MUTATION): in event handlers, effects, cleanup,timer and promise callbacks, and local helpers that receive the value. Passing
a mutated array back to its setter does not re-render, and copying only the
outer object leaves identity-based consumers stale and rewrites the value that
transitions and
useOptimisticrevert to. Pass a new value instead, such assetItems([...items, item]), or keep mutable objects inuseRef. Arraymutators are now also recognized on nested literal properties and on state
initialized lazily or through
useReducer,MapandSetmutators on statecreated with
new Map()ornew Set(), andObject.assign-style targets;render-time mutations keep
OCTANE_STRONG_RENDER_SNAPSHOT_MUTATION.Compatibility mode is unchanged.
dc4800b: Strong mode now rejects a deferred state update computed from the same state'srender snapshot (
OCTANE_STRONG_STALE_STATE_UPDATE). After anawait, or in asetTimeout,setInterval,requestAnimationFrame,requestIdleCallback,queueMicrotask, or promise callback,setCount(count + 1)can overwrite anupdate that happened in between. Locals computed from the snapshot before the
await, copied aliases, and closures are followed. Use the updater form,setCount((current) => current + 1), or read the latest value with the stategetter. Synchronous handlers such as
onClick={() => setCount(count + 1)}remain valid. Compatibility mode is unchanged.
752028d: Strong mode now rejects auseSyncExternalStoregetSnapshotorgetServerSnapshotthat provably returns a new object or array on every call(
OCTANE_STRONG_UNCACHED_STORE_SNAPSHOT), such as a literal, a spread copy,.map(),.filter(),Object.keys(), a standard constructor, or asame-module function or local constant that allocates. Snapshots are compared
with
Object.is, so such a component warns in development and then rendersuntil the update-depth limit throws. Return a value the store keeps, or read
each field with its own
useSyncExternalStorecall. Compatibility mode isunchanged.
dc4800b: Strong mode now rejects write-only state (OCTANE_STRONG_WRITE_ONLY_STATE): astate tuple whose value is elided, unused, or read only to compute its own next
value, whose getter is unused, and whose setter is called or passed on. This is
the force-update pattern, including
useReducer((x) => x + 1, 0)anduseState(0)[1]. It reads the external source during render and subscribesafterwards, so a change in between is never rendered. Subscribe with
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)instead.Compatibility mode is unchanged.
7422815: Universal renderers now ship host-binding and template-program code only when they use it. A Three scene's production bundle is about 5.2 KB gzip smaller.universalHostBinding()installs the subscription, flush, and binding-only transaction code on its first call, so roots in an app that never creates a binding no longer carry it. Binding behavior is unchanged.templateProgramMount,templateProgramRuns, andcollapsedTemplateMountcapabilities take effect only when the driver also setstemplates: universalHostTemplates, a new export fromoctane/universalandoctane/universal/native. A driver that declares these capabilities without it mounts and updates those trees through ordinary host commands. The Lynx background driver passes it.Patch Changes
fbf6e40: Remove the server content left after a component's content during hydration. When the server rendered more in a component's range than the client renders there, the client adopted the matching prefix and kept the rest on screen without a report. This happened for a component whose identity differs on the client, for a hookless component in a renderable hole, and for a component that returns less on the client, including one rendered as a list item. Hydration now discards the stale remainder and reports it once throughonRecoverableError, plus a located development warning. The nodes the client adopted keep their identity. To find where a multi-root template's roots end without parsing it in production, the compiler now passes the template's root count totemplate().a50b846: When hydration adopts an element whose children are all component calls butthe server rendered other content inside it, the first call now reports the
server node it found in place of its range once, the element's server children
are discarded, and the components are built inside the adopted element. A
single-root component used to compare its template with the element itself,
reporting that element, keeping the stale server children, and appending its
output after them; a lite component removed the adopted element entirely. Later
sibling calls in the same element no longer discard the rebuilt content of an
earlier one, and an element the server left empty reports "nothing".
9ef7385: Fix a crash when a pending<Hydrate>boundary resumes a component thatsuspended inside content its parent opened in a server-rendered row.
A deferred boundary's retry resumes the component that suspended before it
renders the rest of the boundary. When a hookless sibling component followed
that component, for example a later row of the same list, the retry picked the
sibling instead and threw
TypeError: Cannot read properties of undefined (reading 'clear')once the promise settled. The retry now resumes thecomponent that suspended.
6ea582e: Hydrate a component call at a hole in an@ifor@switcharm when the server rendered another arm with the same static roots, for a call that cannot render in place of one server node. The server node at the call's position is an element of the other arm, not the component's range. A keyed or dynamic call, or a component that renders several roots, a fragment, or nothing, left that element on screen. A call after one of the arm's adopted roots also deleted that root. A component with several roots in a development build lost the adopted node after the hole. The call now replaces exactly the server node at its position, the arm's adopted nodes keep their identity and keep updating, and hydration reports the mismatch once throughonRecoverableError.52c9d33: Discard the server content left after a hydrated@ifor@switcharm that ends with a@trywhose body throws on the client. The boundary builds its catch arm and leaves the hydration cursor on its own close marker, so the server components the server's longer arm rendered after it stayed on screen with no report. Hydration now looks past that marker, removes them, and reports one structuralonRecoverableError.8f8349b: Compile a bare-left@for (item of items)or@for ({ id } of items)header instead of crashing. Each row binds its own item, as aletheader does, and shadows any outer name, on the DOM client, the server, hydration, split<Hydrate>boundaries, and the universal and Valdi renderers. A header that would assign an existing target, such as@for (obj.x of items), now reports a located compile error.f355acd: Keep a binding island's server content and input while its client data is still loading at activation:@tryin a'use dom bindings'view no longer replaces server-rendered content with@pendingwhen the client's own read is still pending. Like the renderer's resolved boundary, the server content stays and hydrates in place when the read settles; its handlers and mount-only effects start with that commit. After the first commit, a later pending read shows@pendingas before.@tryon its first render no longer loses input. Clicks captured before activation, and input that reaches the island before its data arrives, now wait for the first commit and then replay in order. Input held by an island that is disposed before it commits is dropped.3ffd8cc: A'use dom bindings'view whose text leaf casts a signal handle tonumber({count$ as number}, or a handle passed through props) now server-renders the handle's text, soadoptBindingscan adopt its own SSR output instead of throwing a mismatched-topology error. The ordinary renderer hydrates the same output in place.9350268: A DOM binding program, such as an island view, no longer ends silently on any error namedScopeDisposedError. It ends quietly and keeps its last DOM only when the owner of a source it observes retires. Observed sources are a tracked.get()read, a bound signal handle (concrete, declared, optimistic, or accepted by a transition), a whole-style or projected binding, and a bound control. A read of a different, already retired scope, or any error that only shares the name, is now reported throughreportError. Like any other failure of a committed program, it keeps the last DOM and later updates still apply.4212874: Keep the hydration range-marker validator out of apps that never callhydrateRoot. A dynamic{x}text hole that may hold a signal handlepreviously retained the binding-marker protocol in every client bundle, about
600 B gzip. Adopting a binding view's server text range is now a hydration
method, so client-only apps drop it. Hydration behavior is unchanged.
f03d812:'use dom bindings'views may now declare no props parameter. A view that reads only module-scope signal handles, such as an island's state, compiles foradoptBindings,mountBindingsand server rendering like a view with props. Views with more than one parameter are still rejected.3315f74: Keep hydration aligned after a@try,<ErrorBoundary>,@for, or<Activity>. A@tryor<ErrorBoundary>left the hydration cursor inside its own server range, so a following component with several roots in a body without a template, such as<>@try {…}<Pair /></>, did not find its server range: it was rendered a second time beside the server's copy and reported a false mismatch, even when the server and client rendered the same props. Every boundary, list, and activity now steps past its server range as components and branches do. This also lets an@ifor@switcharm that ends with one of them find where its content ends, so elements or text that the server's longer arm rendered after it are removed and reported once instead of staying on screen without a report.f5896c6: Discard the server content that a hydrating@ifor@switcharm leaves after its last component or nested branch. When the server rendered a longer arm, such as one with a trailing element after the same components, that trailing content used to stay on the page with nothing reported. Hydration now removes it, keeps every node the client adopted, and reports the mismatch once toonRecoverableError, with one development diagnostic at the branch.3770c1e: Discard the server elements and text that an@ifor@switcharm leaves unclaimed during hydration. When the server rendered a longer arm, the client arm adopts the start of the server's range. Elements or text after the roots of the client arm's own template, or after the single root that a component in the arm adopts in place, stayed on screen with no report. Hydration now steps past those roots, removes the server content after them, and reports one structuralonRecoverableError, with a development diagnostic at the directive. Every compiled multi-root template now carries its root count, so production hydration finds that end without parsing the template, including for a branch in a helper passed as a render prop.3994e1e: Discard the server content an@ifor@switcharm leaves unclaimed during hydration. When the server rendered a different arm, the client arm adopts the server's range from its start. Server components or ranges left after the client arm's content stayed on screen until the next arm swap. Hydration now removes them and reports one structuralonRecoverableError, with a development diagnostic at the directive. A dormant<Hydrate>boundary whose captures changed before activation discards them without a report.6f019c5: Hydrate an@ifor@switchat a hole in an@if/@switcharm when the server rendered another arm with the same static roots. The hole's position then holds an element or text of the other arm, not the slot's range. The slot used to treat that node as the node after it: a branch with several roots adopted it and the arm's next static root as well, so a node was lost silently, and later updates left the server node on screen or duplicated content. An empty branch kept the server node, a branch after one of the arm's adopted roots deleted that root, and a single-root branch that did not match threwNotFoundErrorfromhydrateRoot. The slot now takes the place of exactly that server node. It adopts the node when its branch is that node, otherwise it rebuilds over the node or removes it and reports the mismatch once throughonRecoverableError. The arm's adopted nodes keep their identity and keep updating.6c50c70: Read a component's signal and query declarations from its own@try,@pending,@catch,@ifand@switcharms and keyed@forrows without creating a secondcell. A query declared in setup and read again from one of those frames, directly
or through a local
derived$, now starts one request on the client and resumesthe server's request during hydration, instead of starting another request per
arm or row. A component's own signal displayed in an arm also keeps its value
when that arm remounts.
An arm or row still owns the declarations it evaluates itself and resets them
when it is removed, and a child component that receives a handle as a prop still
resolves it in its own instance.
de31710: Cache aconstthat only another cached calculation reads. Inconst labels = formatRows(rows); const view = wrapRows(labels);with onlyviewin the template,labelsused to recompute on every render, soview's cache never hit andwrapRowsran on every render too. Client builds now cache every link of such a chain, keyed on the chain's own inputs, in@{}andreturn <jsx>components alike. That includes aconstread only by a Strong inline render expression. Declarations that are never cached (hook calls, live member calls,let, values only an event handler reads) still never are.Also fix a render crash: a cached calculation whose callback read a
constdeclared after it, as inconst reader = make(() => labels); const labels = formatRows(rows);, threwCannot access 'labels' before initializationbecause its dependency check readlabelsearly. Such a calculation is no longer cached.fbaf501: Release the remaining host ref array owners when a callback cleanup throws, including nested arrays, and preserve the first error for existing error delivery.70ee6a3: When hydration finds other server content in a range where the client rendersseveral siblings, such as server text in a
{hole}whose client value is acomponent that renders two components, the development console now logs one
hydration mismatch diagnostic for that recovery instead of one per sibling.
Once a recovery reaches the end of the range, by discarding the server content
before it or by rebuilding a single-root clone over the last server node,
later sibling components, fragment clones, and single-root clones that find
that end build on the client without a second "the server rendered the end of
the parent block" diagnostic. A mismatch in a separate range still reports on
its own.
ab2798e: Mount and update every component without allocating a closure context. The hydration path for a component inside a client-rebuilt subtree now calls a separate helper, so ordinary component calls no longer capture their arguments up front. Rendering behavior is unchanged.95421bb: Asignal$,derived$, orquery$declared inside a custom hook now belongs to each call of that hook. CallinguseUser$(a)anduseUser$(b)in one component previously shared one cell, so the second call showed the first call's data and never ran its own loader. Each call now owns its cells, a call skipped by a condition keeps its own, and server and browser builds key them identically for SSR seeds and streamed results. Call sites are keyed in modules with a runtime signals import and for hooks named with$.9168bf6: Fix a runtime host element (fromcreateElement) duplicating its children whenhydration retries after suspending. When the server rendered other children than
the client's, the first attempt built the client's children inside the adopted
host; the retry then built a second copy beside them. The retry now replaces the
earlier attempt's children, and a root that retries restores the server's.
A child host element that replaces server content now calls
onRecoverableErroronce across retries, and in development warns once at the hole that renders the
runtime host.
11a9da9: Adopt the server's nodes when a list item suspends while a deferred<Hydrate>boundary hydrates. A component that returns JSX passes the boundary its children as descriptors, so a host's children there hydrate as a list. When the first item's first render suspended, the boundary's retry filled that list again from wherever the resumed render had left the cursor. It reported a hydration mismatch, rebuilt the item on the client, and duplicated server nodes such as the item's first root. The retry now adopts the list from its first server item, as a compiled@fordoes. An item whose render suspends inside a hydrating@ifor@switcharm the server rendered no range for also leaves the server's nodes as they were, so the retry adopts that arm instead of rebuilding it.92495b9: A@catchparameter written as an object or array pattern, such as@catch ({ message }: Error, retry), now binds its names on the client. Previously the client error arm threw aReferenceErroror read a component local with the same name, while server rendering showed the error, so client render and hydration disagreed with SSR. Pattern defaults may read component locals, as in@catch ({ message = fallback }).bfed959: On an ordinary page exit,installSignalDocumentLifecyclenow unmounts independent islands before it retires the document's signal owner. Island cleanups can read their document signals instead of hittingScopeDisposedError, and live islands are no longer notified of the retirement. The owner still retires if an island's unmount throws.c016c2d: Discard and report the server's@if/@switcharm when the client's arm renders nothing during hydration. An@ifwith no client arm already discarded the server content and reported it as an empty branch, but a client arm with an empty body, such as@else { <></> }, left the server's other arm on screen with no report. Hydration now removes that content and reports one structuralonRecoverableError, with a development diagnostic at the directive. When the server arm starts with an element or text node rather than a range, it is still kept.21c3bfe: When hydration rebuilds an@forthat the server rendered with no items becausethe client has items, the development diagnostic says the server rendered
an empty list (@empty)only when the server rendered that arm. A list withoutan
@emptyarm now reportsan empty list.320f9e3: Mount rows with event handlers without per-row weak-map bookkeeping. A module that can receive signal handles recorded each native handler's signal authority in two weak maps, so every mounted row paid two weak-map insertions even when no signals were in use, which made prepending 100 rows to a 1,000-row keyed list about 1.6 times slower. The authority now lives on the host beside its handler, and a scope's published-authority mark is a field on the scope. Updating a text hole with a plain value in such a module also writes the text directly instead of going through the signal binding path.da9b1e4: Preserve the first script or stylesheet preload's options when equivalent calls are deduplicated, so a later preinit inherits the original integrity and connection metadata. Explicit preinit options continue to take precedence.4ab33b6: The compiler now rejectsfor…inand C-style@forheaders on every target.@for (const k in object)used to compile asfor…ofover the object on theDOM client, server, and universal renderers, rendering its values or nothing
instead of its keys.
@for (let i = 0; i < n; i++)crashed the DOM compilerwith an internal
TypeError. Each now reports a diagnostic at the directivethat suggests the
for…ofspelling, such as@for (const name of Object.keys(object); key name).8e90048: Keep every root of a hydrating@ifor@switcharm that suspends inside a deferred<Hydrate>boundary while the server rendered no range for it. When the arm's last root was static, after the component that suspended or after ause()in the arm itself, the boundary's retry still bounded the arm after its first roots. Hydration looked right, but switching to another case left the arm's later roots on screen, and a case change while the boundary was pending did the same. The arm now ends after every root its template adopted, both when it first suspends and when the boundary retries it.6bb20ac: Keep the server nodes that a@switchor@ifarm adopted when the armsuspends inside a deferred
<Hydrate>boundary before it has a range of itsown.
When the server rendered no range for a
@switchor@if, the client's armtakes the place of the server node at that position and adopts it. If that arm
suspended inside a
<Hydrate split={false}>boundary, the boundary's retrypublished the arm's range after the sibling that followed it, so that sibling
reported a false mismatch and built a second copy of its server node. A case
change while the boundary was still pending removed the wrong server nodes, with
the same result for the next sibling. The arm now marks where its content starts
with a comment of its own and keeps track of how far that content reached. Its
retry finalizes the range from there, and another case replaces exactly that
content and renders on the client without reporting a mismatch. This works for
arms whose content is text, several roots, or a root the arm had not yet cloned.
6959fa0: Rebuild a hydrated fragment that starts with a hole when the server rendered something else there, such as another@ifarm. A leading component call, text hole, or nested block matches any server node, so hydration adopted the other arm's nodes as the fragment's roots: its static roots were never built, and a component in the hole took one of the server nodes as its position. Hydration now compares the first static element root after the leading holes with the server node it would adopt. On a mismatch it rebuilds the fragment on the client, as it already did for a fragment that starts with a static root, and reports oneonRecoverableError. The development warning names that static root and the server node found in its place. Matching fragments adopt their server nodes as before. AHYDRATION_RANGE_BOUNDARYpassthrough root, whose owner may adopt one range level off, keeps its existing behavior.105e0f0: Run a hook with an omitted dependency array on every render when its callback reads a binding declared after the hook call. That covers a laterconst,let,classorvar, and theconstthat receives the hook's own result, such as auseCallbackthat calls itself. The compiler read the binding where the hook is called to fill the inferred array. Alet,constorclassthrewReferenceError: Cannot access … before initializationthere. Avarwas stillundefined, so the hook never re-ran when it changed. React runs a hook with an omitted array on every render, and Octane now does the same in this case. Strong mode reportsOCTANE_STRONG_UNTRACKED_EFFECTinstead, asking for the declaration to come before the hook.0d91b93: Give a component rendered inside an@ifor@forarm of a hookless componentthe same signal instance on the server and in the browser. A hookless
component's arms, rows and value children no longer lose that component's level
from their signal and query identity, and no longer gain a stray list-item
segment. Hydration now resumes the server's cells for them instead of starting
fresh ones. Hookless siblings also keep their own owners when hydration retries a
suspended
@trybody.7698907: Adopt a server-rendered@catcharm (or JSXErrorBoundaryfallback) during hydration when the client's try body throws again, instead of reporting a hydration mismatch and rebuilding it. This covers a boundary that is a component's only output and a try body that renders a host element before it throws. The server now marks a caught arm, so the client replays the try body without claiming the catch arm's DOM. A body that renders or waits on the client replaces the server's catch arm without a mismatch report. The replay also reads theuse()values of Suspense boundaries andHydrateislands the server completed inside the abandoned try body, including streamed ones, so a body with a nested boundary adopts the catch arm immediately instead of waiting for client data. When a boundary at the top level of a root rebuilds during hydration, the root's next sibling component now hydrates in place instead of being rebuilt with a mismatch report.3baa492: Hydrate a server-rendered@catcharm when a template-less component'suse()rejected on the server. The component now reads its rejection seed, so the boundary adopts the server's catch arm and reports the caught error instead of discarding that arm, reporting a hydration mismatch, and staying suspended. A component that renders where the server rendered something else still reports the mismatch.1fa3853: Hydration no longer reports a mismatch for a@trybody or<ErrorBoundary>child that throws to its catch arm after adopting server content. A component that cloned its element and then threw from a hole was compared against whatever the server rendered there, often the server's own catch arm, and logged a development mismatch warning and calledonRecoverableErrorin development and production. The catch arm replaces that content, so these reports are now dropped. A body that completes or suspends still reports its mismatches, and nested boundaries pass them to the enclosing boundary.62e76bf: Load the browser's own query selection when hydration finds a server result for adifferent request. When a component's props or state select a query request
other than the one the server resolved under the same owner, hydration no longer
fails with "The presented query definition does not match". The query loads its
current selection as if the server had not seeded it, and adoption keeps the
server's nodes while reporting changed text as a recoverable hydration mismatch.
a1f136c: Deferred hydration no longer lets a live interaction overtake one capturedbefore hydration. A
<Hydrate>boundary or independent island replays itscaptured events after its hydration commits, which can be a frame later. An
event that arrived in between reached the hydrated handlers immediately, so
clicking option A before hydration and option B during it ran B's handler first
and left A selected. The boundary now keeps capturing until its replay runs, so
handlers see input in the order the user produced it. An island whose content
is still suspended after activation also keeps those events instead of
dropping them on the inert server markup.
39f3e97: Deferred hydration now replays captured interaction events with the originalevent's
timeStamp. Each replay used to be a newly constructed event stampedwith the replay time, so the time between a captured
pointerdownandpointerup, or how long an input had been held, collapsed to the hydrationdelay. A boundary nested inside another boundary reset the clock a second time.
Replays from
<Hydrate>boundaries, nested boundaries, and independent islandsnow keep the captured value and remain untrusted (
isTrustedisfalse).11aebc7: Hydrate a single-root component whose body is an@ifor@switch, called where the server rendered another branch's markup. Hydration renders such a call in place of the server node it finds there, but the branch inside the component bounded its content against that node. When the branch's root did not match the node, recovery rebuilt the root and removed the node, andhydrateRootthrewNotFoundErrorand left the container empty. This happened in development and production builds, for an element or component arm, including when the call was its host's last child or the last root of an arm's fragment. When the root matched, hydration adopted it outside the branch's range, so switching the branch to another arm left the server's node on screen next to the new arm. The call now renders before the node after the server's node, so the branch bounds exactly its own root. A mismatch is still reported once throughonRecoverableError.416882f: Hydrate the component after one that adopted a server element in place. When the server rendered a different@if/@switcharm, a component that found that arm's element where its own server range belonged adopted it, but the next component adopted the same element again, and the server's range for the next component stayed on the page. When the first component's own holes left the cursor inside the element, the next component claimed the enclosing block's range instead, which discarded the arm. Hydration now continues after the adopted element. A returned single-root component that hydration rebuilt at the end of its parent's server range no longer takes that range's end marker as its own, so a later render that hides it removes it, and showing it again no longer throws.3ec43da: When a component call finds no server range of its own, because the server rendered another@ifor@switcharm there, and hydration adopts the server markup in place, a component call inside that component's template now adopts the server node at its own position. Previously, when the server had rendered that inner call's markup inline, the inner call rendered against the first server node of the outer template instead: it reported a mismatch and replaced that node, which the outer component had already adopted.0b833bb: Adopt the server's text at a renderable hole in a fragment that a component adopts in place during hydration. When a component call finds no server range of its own, because the server rendered another arm or component inline there, its fragment template adopts those server nodes in place, and the server framed none of that markup's holes. A{value}hole then took the server's text node as its own end marker and inserted a second copy before it, so the page showed the text twice. The hole now adopts that text, or the element an element value renders, and updates it in place. A{null}hole at the end of such a fragment no longer claims the server node after it as the fragment's content, so hydration removes that stale node and reports it once instead of keeping it on screen.11aebc7: Hydrate a component call that has no server range of its own when a component inside the callee's branch adopts the server node at the call. This happens when the server rendered another branch's markup there, and an arm of the callee's@ifor@switchrenders a component whose root matches that node. Hydration rendered the inner component in place of the node, but the call itself did not record that it had adopted the node. The page looked right after hydration, but two things went wrong. Swapping the component that the call renders, for example through an imported binding, left the old component's content on screen next to the new component. When the call was the whole arm of an adopted branch, the server nodes after the adopted node in that arm were never removed. The call now adopts the node with the component inside it, in development and production builds. A swap replaces the call's content, and the leftover server nodes are removed and reported once throughonRecoverableError.bc30a18: Remove the server markup left after a component's last call when that call adopted its server node in place. When the server rendered another component's markup in a component's range, and the client's content there is a sequence of calls without server ranges of their own, each call adopts its server node in place. Whatever the server rendered after the last call's nodes stayed on screen, and hydration reported nothing. Hydration now removes it and reports the mismatch once throughonRecoverableError, while the nodes the calls adopted keep their identity. This covers single roots and fragments, in lite and full component slots, in development and production builds.b319c16: Production builds emit less code for inlineduseMemoanduseCallbackcalls. When the compiler supplied the memo's slot, the hook's name is no longer passed to the runtime, since only an authored slot can be missing. In plain.tsand.jsmodules, a dependency that is a never-reassigned local binding is now read directly instead of being copied to a temporary first. Across the repository's binding modules, this saves about 5.6 KB gzip, summed per module.bc6761f: Thehydrate: 'islands'shell check now rejects more interactive shells and also runs in dev:useEffect as onMount) and through a namespace or member call (O.useState()), and so is an aliased signal declaration.render={Item}) and a helper called to render output. A relative import passed into JSX is checked when it can render (a function, or a component-named value), including a namespace member such asUI.Buttonand a module-level alias of a local function. Strings, asset URLs and other plain values, including any export of a non-Octane module, are not shell output. A package component or a module-level wrapper such asmemo(...)passed as a value cannot be checked.{state.count$}), an import alias (count$ as live) or a module-level alias (const live = count$) is reported.f5644f7: Stop a query below a JSX<Suspense>from reloading without end during hydration, and resume the server value that a.tsxcomponent reads in its returned JSX. A JSX<Suspense>or<ErrorBoundary>gave its try body a new identity on every render. A boundary retrying its own suspended first attempt therefore looked like it had received new children: it retired the attempt's queries and started fresh ones. When the browser had to load a query, for example one without a server seed, each load resolved into another retry and another load, and the page re-rendered indefinitely. The boundary now keeps one body and carries its children like a compiled@try, so only new children restart the attempt. On the server, a component's returned value rendered outside that component's signal owner, while the client renders it inside. Aquery$that a.tsxcomponent reads with.get()in its returned JSX was therefore seeded under the parent's identity and loaded twice on the server, and the browser loaded it again with a hydration mismatch. The server now renders a returned value inside its component's owner.5ef4f9a: Render private static JSX components through their compiled fragment again.A private
.tsx/.jsxcomponent whose body returns only static JSX, andwhose every use is an attribute-free child of a returned host element, again
renders through that parent's template as a lite component call. Since async
signals, component descriptors carry their invocation site, and the proof
recognized only the older descriptor call. Every such component therefore fell
back to a full component slot plus a descriptor child slot. On the
signal-favoring chain, the JSX twin's shallow bump went from 10 full component
slots to 100.
18c1b77: Reorder keyed lists without journaling every row. During a root render that can still be rolled back, a keyed reorder recorded each surviving row's previous position, four journal slots per row. Rotating a 1,000-row list was about 1.4 times slower than before root renders became undoable. The list's own shape record now restores each row's position from its original order on rollback. Recording the key order for that record also uses a single bulk copy. Rows that render their index still show the right position after a held render retries.2b69387: Discard a mismatched server node inside a hookless component's host during hydration. A hookless component renders into the element its call sits in, which can differ from the parent of the enclosing block's range, for example a<section>inside an@ifarm. When its template did not match the server node there, hydration kept the stale server node on screen and inserted the client's element beside it. Hydration now builds the client's element in that node's place, including when the node was the host's last child, and reports the mismatch once.07cc1d2: Keep a component-localderived$orquery$when its captured values are unchanged. Since local declarations started following new props, every render ran the declaration's closure again, even with equal props. Aderived$that returned an object produced a new object on each render, so an effect or memoized child that depended on it ran on every parent render. The compiler now lists the render values a local declaration captures, as it does for a hook with an omitted dependency list. A render whose captured values are all unchanged keeps the accepted definition and its value without running the closure, and skips the per-render staging work. A changed value still reruns the declaration as before. A handle from a localsignal$with no key or a literal key is not a captured value, so aderived$that reads one keeps its value until the signal changes. When two declarations name one cell, such as a repeated explicit key, the first declaration in a render presents it on the server as well as in the browser. Server rendering previously threw because the second declaration replaced the cell's computation during the render.Declarations reached through a custom hook also no longer share a cell with an explicit key that spells out the hook's call path, such as
'user/h:…'declared directly in the component, and their path is no longer rebuilt for each declaration on every render.3d0e5e2: Re-select a component-localquery$when a later render's selector captures new props or state, and recompute a synchronous component-localderived$from its captured values. Previously both kept the closure from their first render. An equal selection keeps its request and adopts the new loader without refetching. A changed selection aborts the obsolete request and starts the new one when the render is accepted. A held transition or suspended attempt keeps committed readers on the accepted selection and reuses its pending request when it retries.f647cef: Keep destructured.map()callback parameters writable on the client. Atemplate
xs.map(({ label }) => <li … />)compiles to a keyed row loop, and itsdestructured fields were re-declared with
const, so a row handler thatreassigned one, such as
onClick={() => { label = label + '!' }}, threwTypeError: Assignment to constant variableafter mount or hydration. Serverrendering already treated them as ordinary parameters. Identifier parameters
were unaffected.
The same row prologue now keeps the kind of an authored
@forheader, so thefields of
@for (let { label } of xs)can be reassigned too. Output forconstheaders and identifier parameters is unchanged.8e90048: Keep every root of a hydrating@ifor@switchbranch that the server rendered no range for. Such a branch adopts the server nodes at the cursor in place, and hydration bounded its content after the first root it adopted. When the branch was the whole content of another arm or of a component the client adopted, the rules that remove the server's leftover content then deleted the branch's later roots, which the client still renders, and reported a mismatch for them. A branch that was not deleted from still owned only its first root, so switching it off left the others on screen. The branch now ends after all the roots its template adopted. The server content after them is still removed and reported once, and a branch whose roots outnumber what the server rendered there is built on the client and reported, instead of hydrating without its later roots. This also covers a branch inside a component that was adopted in place.d496796: Compare a memo component's plain props with fewer ownership checks. The memo bail looked up own-property membership for every prop, so that an inheritedObject.prototypevalue cannot stand in for a removed prop. That lookup is now made only for values such a read can produce:undefined, functions and objects. A primitive prop that compares equal is already the object's own. Rows that pass mostly strings and numbers bail with one lookup instead of one per prop. An own__proto__prop holdingObject.prototypestill counts as a change when it is replaced.fdf54fe: A plain.tsor.jsmodule that calls a hook-named method while it initializes, such asexport const initial = store.useValue(), now evaluates instead of throwingReferenceError: Cannot access '_h$0' before initialization. That covers top-level statements, optional chains, and a module-level class's static fields, static blocks, and computed keys. These calls run outside every render, so they keep their authored form with no hook slot. A hook call in an instance field initializer runs with each construction, possibly during a render, so a hook method or module-declared custom hook called there now keeps its own slot, as it would in a constructor.ce97a96: A plain.tsor.jsmodule that calls a hook-named method or a custom hook inside a function it invokes in place, such asexport const value = (() => store.useValue())(), now evaluates instead of throwingReferenceError: Cannot access '_h$0' before initialization. That covers arrow and function expressions called directly or through.calland.apply, including async ones. A generator body waits for.next(), which a render may call, so its hook calls still get their own slot, as do those in a function invoked in place inside a component or hook.dc3e180: Drop the native array snapshots from client and server bundles that never map alist. Both runtimes record
Array.prototype.mapand theArray[Symbol.species]getter when they load, so a mapped list can tell the native
mapfrom one thatuser code installs later. Bundlers could not prove those two reads free of side
effects. Every bundle that never reached the mapped-list code therefore kept
them as dead top-level statements, including a server bundle that only escapes
HTML.
The reads are now marked pure, so bundlers remove them when nothing uses them.
A bundle that maps a list still takes both snapshots at load, before user code
can replace either intrinsic. A minimal client bundle shrinks by 89 bytes raw,
and the smallest server bundle shrinks from 654 to 571 bytes raw.
e219d88: Set host refs before layout effects when a nested Suspense boundary revealsafter an update replaced its parent's pending retry. The nested boundary first
suspended inside that retry; once the update discarded it, the boundary stayed
hidden without its reveal bookkeeping, so refs mounted in it (including hosts
preserved from before the suspension) were never attached and a layout effect
reading them saw
null. A nested boundary that suspends while its parent isalready hidden also no longer calls a callback ref with
nulla second time,matching React.
ed188d7: Leave a signal-capable only-child text hole alone when its value is unchanged.A
{props.value}hole that is its element's only child compiles tobindSignalChildwhenever the value could carry a signal. That binding keptthe Text node as its token, so every render read the node's
nodeValueto tellan unchanged value from a changed one, and an unchanged value overwrote any
outside edit to the text. The binding now keeps the last written primitive, as
compiled text holes did before signal-capable bindings: an unchanged primitive
does nothing and leaves the DOM alone, and a changed one rewrites the same Text
node.
afbad9f: Discard and report server content that an only-child text hole cannot adopt during hydration.A text hole that is its element's only child adopts the server's leading Text
node. When the server rendered something else there, such as an element or a
comment, hydration kept that content and appended the client's text after it.
It reported nothing, and every later update wrote the text beside the stale
server nodes. Hydration now removes that content, so the element holds only the
client's text. It reports the mismatch through
onRecoverableErrorand, indevelopment, a located warning.
suppressHydrationWarningsilences the report,here and when the client renders nothing over such content. An empty server
frame is not reported, the same as an empty element.
09cf476: Preserve the submitter's document order in function form actions and manual transition form status data. Include its entry before the nativeformdataevent fires.0887a68: A delegated event listener that the browser runs synchronously during a render'sDOM writes, such as
onBlurwhen a render disables or removes a focused input,can now write signals. Before, it inherited the render's signal write guard and
threw
SignalWriteError. Its reads no longer become dependencies of that render.Subscribers notified by its writes also run outside the render, so they can
write signals and their updates schedule like any other event's. The rest of the
render stays write-guarded, and a listener called from inside a pure computat
Configuration
📅 Schedule: (UTC)
* 0-3 * * 1)🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.