Repository navigation
Stop the community detail route from updating during render - #921
Conversation
The route's render function applied preloaded loader data and kicked off the stargazers fetch inline. That fetch sets state and calls handle.update() before its first await, but the runtime only wires a component's update scheduler after its first render returns, so the call threw 'scheduleUpdate not implemented' as an unhandled rejection (KODY-CLOUDFLARE-10). Queue the load as a task instead, matching how the route already defers its detail-shell load. Also fail the SSR hydration journey on any client-side page error, since a throw in the client runtime silently kills the rest of hydration. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Remix 3 beta's client runtime reads window.navigation.updateCurrentEntry() unguarded while it boots, so run() throws before a single component mounts on every browser that does not ship the Navigation API: Firefox below 145 and all iOS WKWebView browsers, including Chrome for iOS and in-app browsers. Those visitors lost the entire client bundle, and because the throw happens during boot it also took out the handler that would have reported it (KODY-CLOUDFLARE-17). Install a minimal stand-in before run() when the browser has no Navigation API. Kody never uses that API — client-router.tsx drives SPA navigation through history.pushState and a document click handler — so a stand-in that never emits 'navigate' behaves exactly like the real thing here. Only the members the Remix runtime reads are implemented; this comes out once the upstream guard ships. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
📝 WalkthroughWalkthroughAdds a Web Navigation API fallback for unsupported browsers, installs it before client runtime startup, expands SSR hydration coverage, and defers initial stargazer loading until after community detail rendering. ChangesNavigation fallback and hydration stability
Sequence Diagram(s)sequenceDiagram
participant Browser
participant ClientEntry
participant NavigationApiFallback
participant CommunityRouter
Browser->>ClientEntry: start client entry
ClientEntry->>NavigationApiFallback: install fallback
Browser->>CommunityRouter: click community listing
CommunityRouter->>NavigationApiFallback: navigate to detail
NavigationApiFallback->>Browser: assign or replace document URL
Browser->>CommunityRouter: hydrate detail route
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Firefox shipped it in 147 and WebKit in 26.2, so the affected set is Firefox 146 and below plus every browser on iOS 26.1 and below. The Sentry events are Firefox 141 and iOS 26.1, which both land inside that range; the earlier note guessed 145 and blamed WKWebView specifically. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
|
🔎 Preview deployed: https://kody-pr-921.kody-a99.workers.dev Worker: Mocks:
|
Verified against real Firefox 146 (which has no Navigation API) on production: the throw lands after createFrame, so hydration already in flight survives and the client router still handles clicks. The real cost is an unhandled TypeError on every page load plus the missing error-handler wiring in entry.tsx, not a dead client bundle. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Kent does not want to carry a local workaround for the Remix bug while the upstream fix is in play, so the KODY-CLOUDFLARE-17 stopgap moves to its own PR pending remix-run/remix#11641. This PR keeps only the KODY-CLOUDFLARE-10 fix, which is our own update-during-render bug. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 56f23fd. Configure here.
| // up a component's update scheduler after its first render | ||
| // returns, so calling it inline throws instead of scheduling: | ||
| // https://github.com/remix-run/remix/issues/11642 | ||
| handle.queueTask(() => void loadStargazers(listingId)) |
There was a problem hiding this comment.
Stale listing in deferred load
Medium Severity
Deferring loadStargazers with handle.queueTask captures listingId from the render that applied preloaded shell data. If the user navigates to another community before the task runs, the fetch can finish for the previous listing and update stargazer state while the UI shows a different package, unlike loadDetailShell which reads the current listing when its queued task executes.
Reviewed by Cursor Bugbot for commit 56f23fd. Configure here.


Fixes KODY-CLOUDFLARE-10 —
Error: scheduleUpdate not implemented, unhandled,auto.browser.global_handlers.onunhandledrejection, firing on/community/<id>across Chrome, Edge, and iOS.The bug
CommunityDetailRoute's render function callsapplyRouteShellData, which kicked offloadStargazersinline. That function sets state and callshandle.update()before its firstawait.The Remix reconciler only assigns a component's update scheduler after its first render returns:
So the call hit the default
#scheduleUpdate, which throws. And becauseupdate()builds its promise withnew Promise(executor), the throw became a rejected promise — surfacing as an unhandled rejection with no component name and no hint that the real problem was "you updated during render".This is our code misusing the framework, not the framework failing us. Queuing the load as a task fixes it and matches how the same render already defers
loadDetailShella few lines below.I searched the rest of
packages/worker/client/for the same shape — a helper reachable from a render function that callshandle.update()before awaiting — and this was the only instance.Verification
Reverting just the
community-detail.tsxchange and re-runninge2e/ssr-hydration.spec.tsfails with three occurrences of the error across the journey:With the fix it passes. I also confirmed the framework-level cause independently by making an unrelated route call
handle.update()in its first render, which reproduces the identical error — that reduction is filed upstream as a DX bug (remix-run/remix#11642), since the message makes this needlessly hard to diagnose.Rather than adding a narrow regression test, the existing SSR hydration journey now fails on any client-side page error. That covers this bug and any future throw anywhere in that flow, which matters because a client runtime throw silently truncates the rest of hydration instead of failing loudly.
npm run validatepasses.System recap — composes existing primitives (low risk)
Mode: recap · Base:
main@dc25e719· Head:56f23fdbClassification: composes — no primitive changes shape or contract; one route
moves a data load off the render path and an existing E2E journey gains an
assertion.
Primitives touched
community-listingsSystem map
The community detail route defers its stargazers fetch off the render path; the endpoints it calls are unchanged.
Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context (unchanged, included only when an edge crosses it).
Before / after
/community/<id>first render with preloaded loader dataloadStargazerscalled inline →handle.update()throws → unhandled rejectionInvariants
No change to per-user isolation: the stargazers load still goes through the same credentialed, session-scoped endpoint, only later in the same render cycle.
Summary by CodeRabbit
Bug Fixes
Tests