Repository navigation
Upgrade Preact to 11.2 with unmount lifecycle fixes - #249
Merged
Merged
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Layout cleanup leaves queued intersection callbacks able to prefetch after a Link unmounts; an active-state guard and a test delivering a queued notification after removal are needed.
1 open issue
What changed in this PR
Upgrades the shared runtime to Preact 11.2.0, replacing the dependency-only bump proposed in #246 with compatibility updates and unmount lifecycle protections.
Changes:
- Removes
SuspenseListexports and exposesuseanduseEffectEventthrough both compatibility entry points. - Moves mount-owned cleanup to layout effects and prevents pending guards from acting after Router unmount.
- Synchronizes all four lockfiles, updates export and unmount assertions, and documents the migration.
| File | Description |
|---|---|
site/bun.lock |
Resolves the site's runtime dependency to Preact 11.2.0. |
runtime/test/compat.test.ts |
Checks the new compatibility exports. |
runtime/test/browser-entry-compat.test.ts |
Checks the new exports in the built browser bundle. |
runtime/src/router.ts |
Updates Router and Link cleanup and guards against post-unmount results. |
runtime/src/router.test.ts |
Checks base removal immediately after unmount. |
runtime/src/dev-reload.ts |
Removes reload-state listeners synchronously on unmount. |
runtime/src/compat/index.ts |
Removes SuspenseList and exports the new APIs. |
runtime/src/browser-entry.ts |
Aligns browser exports with the updated compatibility bridge. |
runtime/package.json |
Raises the Preact dependency to ^11.2.0. |
runtime/bun.lock |
Locks the runtime to Preact 11.2.0. |
examples/tsx-site/bun.lock |
Synchronizes the TSX example's Preact dependency. |
examples/styling-site/bun.lock |
Synchronizes the styling example's Preact dependency. |
docs/dependency-audit.md |
Records migration rationale and validation results. |
changelog.d/preact-11.md |
Documents lifecycle changes and the breaking export removal. |
Review effort: Balanced
valthon
commented
Nov 9, 2026
valthon
commented
Nov 9, 2026
valthon
commented
Nov 9, 2026
valthon
commented
Nov 9, 2026
valthon
commented
Nov 9, 2026
valthon
commented
Nov 9, 2026
valthon
commented
Nov 9, 2026
valthon
commented
Nov 9, 2026
Preact 11 no longer ships SuspenseList, and both the compatibility bridge and the shared browser entry re-export it, so the Dependabot bump to 11.0.2 in #246 cannot build. Drop both exports ahead of the upgrade. This is a breaking change for callers that import SuspenseList from @z/runtime/compat or the shared bundle. Suspense itself stays available. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HTAHrjUxHvSdfgaDLibY1X
Move every lockfile from Preact 10.29.8 to 11.2.0. The string renderer stays at 6.8.2, which meets 11.2's peer requirement. The compatibility bridge gains use and useEffectEvent. Preact 11 defers passive-effect cleanup on unmount until after paint, so anything that relied on it being synchronous gets a window in which a removed Router or view still acts. Close each one: - The Router's location listener and global base detach from a layout cleanup. Only teardown moves: setup stays passive, because a setState from a layout effect flushes the Router's passive effects before any descendant's and would run the first guard ahead of a child's useEffect. - Pending guards and lazy route loads check a mounted ref that the layout cleanup clears. - The viewport observer and the reload-state listener use layout cleanup. That lazy-load path also had a bug that predates Preact 11: an abandoned load left its rejected promise cached, so the next visit inherited the failure and latched the route on its skeleton. It now drops the promise and the next visit retries. Preact 11 also passes ref to function components in props, so Link merges a caller's ref with the anchor ref its observer needs. A callback ref may return a cleanup, in which case Preact skips the null call, so the merged ref passes that cleanup on. use(promise) cannot be prerendered because the sidecar renders with the synchronous renderToString; docs/spa.md records the limit. The changelog also notes the ref change and compat's version moving to 19.0.0, which alters what npm React libraries detect. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HTAHrjUxHvSdfgaDLibY1X
disconnect() does not retract a notification that is already queued, so a Link could prefetch after it unmounted or after its one observation had already fired. An observation-local flag now rejects those. The observer also closed over ctx.prefetch while its deps named only the mode and href. ctx.prefetch is re-created when the Router's routes change, so a new table with the same href kept prefetching against the old one. Keying on it re-observes, at the cost of observer churn when a parent passes an inline routes literal; docs/spa.md asks for a stable identity. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01HTAHrjUxHvSdfgaDLibY1X
valthon
force-pushed
the
codex/preact-11
branch
from
November 9, 2026 01:13
3d8a0fa to
0bb607e
Compare
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.

Summary
Upgrade the shared runtime from Preact 10.29.8 to 11.2.0. The automatic bump in #246 cannot build because our compatibility bridge exports
SuspenseList, which Preact 11 removed. Removing those exports alone also exposes deferred unmount cleanup: a pending guard can redirect after its Router is removed, while viewport observers and reload-state listeners remain active.SuspenseListfrom both compatibility entry points and expose Preact 11'suseanduseEffectEventAPIs. Breaking change: callers importingSuspenseListmust remove that import;Suspenseremains supported.Linkpasses a caller'srefto its anchor, because Preact 11 delivers refs in props.Caller-visible Preact 11 behaviour, recorded in the changelog: function components receive
refin props, compat'sversionis now19.0.0, anduse(promise)cannot be prerendered because the sidecar renders synchronously.Supersedes the proposed Preact bump in #246.
Documentation & examples sync
docs/dependency-audit.mdexplains the migration and regression checks.docs/spa.mdrecords theuse(promise)prerender limit and theroutesidentity advice for viewport prefetch.Verification
Three commits on
main. Each one, in a clean detached checkout, passed the branding and confidentiality gates, a frozen install, the Bun runtime suite, and the runtime typecheck. The tip runs 826 Bun tests with 0 failures.The new router regression tests were verified to fail without their fixes.
An earlier revision of this migration passed all 41 local Linux workflow verification steps and its nine GitHub CI checks. The final revision differs from it only in router code, tests, and documentation. GitHub CI on this head is the check for the full matrix.
UNVERIFIED: macOS nightly lane; the local host is Linux. The Zig suites, shell e2e, and browser lanes were not re-run locally on this head.
🤖 Generated with Claude Code
https://claude.ai/code/session_01HTAHrjUxHvSdfgaDLibY1X