Skip to content

Stop the community detail route from updating during render - #921

Merged
kody-bot merged 7 commits into
mainfrom
cursor/sentry-k2-kody-remix-client-b35b
Jul 24, 2026
Merged

kody-bot merged 7 commits into
mainfrom
cursor/sentry-k2-kody-remix-client-b35b

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 24, 2026 •

Copy link
Copy Markdown
Member

Fixes KODY-CLOUDFLARE-10 — Error: scheduleUpdate not implemented, unhandled, auto.browser.global_handlers.onunhandledrejection, firing on /community/<id> across Chrome, Edge, and iOS.

Scope note: this PR originally also carried a stopgap for KODY-CLOUDFLARE-17 (the Navigation API crash). Kent would rather fix that one upstream than carry a local workaround, so it has moved to #922 and is not for merge yet. This PR is now only our own bug.

The bug

CommunityDetailRoute's render function calls applyRouteShellData, which kicked off loadStargazers inline. That function sets state and calls handle.update() before its first await.

The Remix reconciler only assigns a component's update scheduler after its first render returns:

// @remix-run/ui — reconcile.ts
let [element, tasks] = handle.render(next.props)   // render fn runs here
// ...diff children...
handle.setScheduleUpdate(() => scheduler.enqueue(committed, domParent))   // only now

So the call hit the default #scheduleUpdate, which throws. And because update() builds its promise with new 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 loadDetailShell a few lines below.

I searched the rest of packages/worker/client/ for the same shape — a helper reachable from a render function that calls handle.update() before awaiting — and this was the only instance.

Verification

Reverting just the community-detail.tsx change and re-running e2e/ssr-hydration.spec.ts fails with three occurrences of the error across the journey:

✘ SSR community HTML hydrates SPA navigation and client search
    + "scheduleUpdate not implemented"
    + "scheduleUpdate not implemented"
    + "scheduleUpdate not implemented"

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 validate passes.

System recap — composes existing primitives (low risk)

Mode: recap · Base: main @ dc25e719 · Head: 56f23fdb

Classification: 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

Primitive Group Impact
community-listings assistant composes — detail route queues its stargazers load instead of calling it in render

System 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).

flowchart LR
	communityListings["community-listings<br/>Community package listings"]:::touched
	appUi["app-ui<br/>Browser app (Remix 3)"]:::untouched
	appSessions["app-sessions<br/>Browser sessions"]:::untouched
	appUi -->|"first render of CommunityDetailRoute"| communityListings
	communityListings -->|"queueTask(loadStargazers) instead of render-time handle.update()"| communityListings
	communityListings -->|"credentialed GET /community/:id stargazers API (unchanged)"| appSessions
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Before / after

Before After
/community/<id> first render with preloaded loader data loadStargazers called inline → handle.update() throws → unhandled rejection Load queued as a task; stargazers render normally

Invariants

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.

Open in Web Open in Cursor 

Summary by CodeRabbit

  • Bug Fixes

    • Improved app hydration and navigation in browsers without the Web Navigation API.
    • Prevented runtime errors when loading community detail pages with preloaded data.
    • Ensured stargazer information loads reliably after page rendering completes.
  • Tests

    • Added coverage for fallback navigation behavior and server-rendered app hydration.
    • Added checks to detect client-side runtime errors during navigation.

cursoragent and others added 2 commits July 24, 2026 19:58
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>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Navigation fallback and hydration stability

Layer / File(s) Summary
Navigation API fallback contract
packages/worker/client/navigation-api-fallback.ts, packages/worker/client/navigation-api-fallback.node.test.ts
Implements synthetic navigation state, assign/replace delegation, guarded installation, and unit coverage for supported and unsupported environments.
Startup installation and hydration validation
packages/worker/client/entry.tsx, e2e/ssr-hydration.spec.ts
Installs the fallback before runtime startup and tests hydration and navigation without window.navigation, including client runtime error assertions.
Deferred community detail loading
packages/worker/client/routes/community-detail.tsx
Queues the initial stargazer load after route-shell rendering instead of invoking it inline.
Estimated code review effort: 3 (Moderate) ~20 minutes

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes one major fix in the PR: deferring the community detail route update during render.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/sentry-k2-kody-remix-client-b35b

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

cursoragent and others added 2 commits July 24, 2026 20:02
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>
@kody-bot
kody-bot marked this pull request as ready for review July 24, 2026 20:08
@github-actions

github-actions Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-921.kody-a99.workers.dev

Worker: kody-pr-921
D1: kody-pr-921-db
KV: kody-pr-921-oauth-kv

Mocks:

cursoragent and others added 2 commits July 24, 2026 20:18
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>
@cursor cursor Bot changed the title Fix two client runtime crashes: missing Navigation API and update-during-render Stop the community detail route from updating during render Jul 24, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 56f23fd. Configure here.

@kody-bot
kody-bot merged commit 569128c into main Jul 24, 2026
5 checks passed
@kody-bot
kody-bot deleted the cursor/sentry-k2-kody-remix-client-b35b branch July 24, 2026 21:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants