Use strict constructor equality and cover the coercion regression without timing assertions. Existing boxed String rendering remains covered.
Developed with AI assistance (Hermes/OpenAI).
Use a dedicated LIS_CANDIDATE bit so interrupted renders cannot leave new children marked MATCHED. Keep the inexpensive flag test in the minimal-move pass and clear the two new-vnode placement flags after diffing.
Add a regression test covering repeated render failures, DOM reuse, ref detachment, and component unmounting. Source and minified suites: 1415 passed, 11 skipped each.
The minimal-move pass (#5174) marked matched new children with MATCHED
and relied on diffChildren's placement loop to clear it. When a child
throws and no error boundary catches it, that loop never finishes. The
new vnodes keep MATCHED and become the old tree of the next render,
where findMatchingIndex and the unmount loop skip them. The next
render then mounts a second copy and keeps the first one.
Derive the minimal-move candidates from `_index` instead, so no flag
is set on new vnodes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
IS_NON_DIMENSIONAL misses aspectRatio, fillOpacity, stopOpacity,
strokeOpacity and strokeMiterlimit. None accepts a <length>, so the
appended px makes the value invalid and the browser drops the whole
declaration: style={{ aspectRatio: 2 }} renders nothing at all.
All five are in React's isUnitlessNumber.
A throwing layout effect or layout cleanup used to clear the render
callbacks of every component in the commit queue, so unrelated, still
mounted components lost their layout effects and their class
componentDidMount/componentDidUpdate/setState callbacks, and any other
error in the same commit was never reported.
Since #2193 the reset was needed because the thrower's leftover hook
states would otherwise be invoked as functions by commitRoot. Only the
throwing component's queue needs resetting, so replace it with a single
callback rethrowing the error: commitRoot then routes it to that
component's boundary through its own per-component catch, after every
layout effect ran. Deferring the routing also keeps an unhandled error
from stranding hook states that a later render would queue and run a
second time.
isHydrating is MODE_HYDRATE or falsy here (render() and renderComponent() pass `_flags & MODE_HYDRATE`, diffElementNodes `false`, portals `null`), so the ternary is redundant.
-3 B gz / -3 B br.
componentWillReceiveProps is called under the same conditions; only the number of `getDerivedStateFromProps` lookups on unchanged props drops (observable only through a static getter).
-4 B gz / -3 B br.
`handled` is now assigned before every read (`ctor && ctor.getDerivedStateFromError`, then 1 after componentDidCatch), and the DIRTY check runs once after both lifecycles, at the same point as before. The catch-side `handled = 0` reset is no longer needed: the next ancestor overwrites `handled` before reading it, so the #5284 carry-over can't come back.
-4 B gz / -1 B br.
rangeEnd() already walks the range, so let it collect the inner nodes
instead of re-walking nextSibling until the end marker. The array is only
allocated for components that own a range (`_excess` set).
-11 B gz / -10 B br.
Once a boundary resolved, nothing referenced its `<!--$s-->` markers, so
unmounting it left an empty pair behind. With streaming that isn't inert: a
`<preact-island>` chunk arriving after the unmount finds the empty range and
inserts the server HTML again, so unmounted content reappears.
Keep `_excess` after the resume (the resume path is gated on MODE_SUSPENDED,
so it is never re-entered) and remove the markers in unmount. A resolved
owner only removes the two markers: its content went with its child vnodes,
and after a keyed reorder the markers may surround a sibling's content.
Resuming stays mutation-free.
preact 4588 -> 4595 B br (+7)
A resumed range starts its cursor at the first kept node. An empty range
(`<!--$s--><!--/$s-->`, e.g. a streamed Suspense without a fallback) has
none, so the resolved content was appended at the end of the parent instead
of between its markers. Fall back to the end marker.
preact 4583 -> 4588 B br (+5)