Skip to content

fix(router): fix loader rerun on params and search change#69

Merged
yzuyr merged 1 commit into
mainfrom
fix/router-fix-params-rerun
Jul 17, 2026
Merged

fix(router): fix loader rerun on params and search change#69
yzuyr merged 1 commit into
mainfrom
fix/router-fix-params-rerun

Conversation

@yzuyr

@yzuyr yzuyr commented Jul 17, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • Bug Fixes

    • Route loaders now refresh correctly when route parameters or query strings change.
    • Hash-only navigation no longer triggers unnecessary reloads.
    • Rapid consecutive navigations commit only the latest loader result.
    • Persisted store state is available synchronously after hydration.
  • Documentation

    • Clarified how loaders should access route parameters and URLs.
  • Chores

    • Updated the router package to version 0.8.6.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Router navigation now revalidates loaders when pathname or search changes within the same route pattern, while ignoring hash-only changes. SPA and hydration tests cover these flows, package versions move to 0.8.6, and persistence gains synchronous hydration coverage.

Changes

Router navigation revalidation

Layer / File(s) Summary
Loader revalidation and mounting
packages/router/src/index.ts
Loader guidance now uses ctx.params and ctx.url; hydration and SPA navigation track pathname-plus-search and remount when it changes.
Navigation coverage and version propagation
packages/router/src/index.test.ts, packages/router/package.json, apps/website/package.json, templates/*/package.json
Tests cover parameter, search, hash, rapid, cross-pattern, and hydrated navigations; manifests update the router to version 0.8.6.

Store persistence validation

Layer / File(s) Summary
Synchronous persistence hydration
packages/store/src/index.test.ts
Adds coverage confirming persisted values are available immediately after persist() is called.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant NavHandler
  participant RouterLoader
  participant RouteView
  Browser->>NavHandler: Navigate to pathname or search change
  NavHandler->>RouterLoader: Revalidate route loader
  RouterLoader-->>NavHandler: Return loader result
  NavHandler->>RouteView: Mount updated route view
  RouteView-->>Browser: Render updated content
Loading

Possibly related PRs

  • ilhajs/ilha#11: Introduced the router loader behavior extended by this change.
  • ilhajs/ilha#14: Related hydration and pathname-plus-search mounting logic.
  • ilhajs/ilha#55: Related store persistence API and test coverage.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main router fix: loaders rerun when params or search change.
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.
✨ 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 fix/router-fix-params-rerun

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/router/src/index.ts (1)

2383-2404: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prevent race conditions when applying navigation state.

When a navigation is superseded by a newer one, the fetch promise could still resolve successfully (e.g., if the abort happens exactly as the fetch completes). To prevent leaking the unmount function or incorrectly resetting currentMountedPath to the older URL, verify that thisNav === navVersion before applying the mount results. This brings NavHandler in line with the race-condition guards already present in _revalidate (line 2478) and the initial loader render (line 2438).

🐛 Proposed fix
-              try {
-                unmountView = await mountRouteWithHydration(
-                  current,
-                  viewHost,
-                  pathWithSearch,
-                  signal,
-                  registry,
-                  reverseRegistry,
-                );
-              } catch (e: any) {
+              let um: (() => void) | undefined;
+              try {
+                um = await mountRouteWithHydration(
+                  current,
+                  viewHost,
+                  pathWithSearch,
+                  signal,
+                  registry,
+                  reverseRegistry,
+                );
+              } catch (e: any) {
                 if (e?.name === "AbortError") return;
                 // Contain navigation failures — an unhandled rejection here
                 // would leave a blank view with no diagnostics.
                 console.error("[ilha-router] navigation failed:", e);
-                viewHost.innerHTML = `<div data-router-view data-router-error="500"></div>`;
+                if (thisNav === navVersion) {
+                  viewHost.innerHTML = `<div data-router-view data-router-error="500"></div>`;
+                }
                 return;
               } finally {
                 settle();
               }
-              currentMountedIsland = current;
-              currentMountedPath = pathWithSearch;
+              if (thisNav === navVersion) {
+                unmountView = um;
+                currentMountedIsland = current;
+                currentMountedPath = pathWithSearch;
+              } else {
+                um?.();
+              }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router/src/index.ts` around lines 2383 - 2404, Guard the successful
mount-result application in the navigation flow with a `thisNav === navVersion`
check before assigning `unmountView`, `currentMountedIsland`, or
`currentMountedPath`. If the navigation is stale, discard the returned unmount
function and do not update mounted state, matching the existing guards in
`_revalidate` and the initial loader render.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/store/src/index.test.ts`:
- Around line 1341-1345: Wrap the assertion following persist(s, "p1b") in a
try/finally block so stop() always executes, including when expect(s.getState())
fails. Keep the existing state expectation unchanged and place the cleanup call
in the finally block.

---

Outside diff comments:
In `@packages/router/src/index.ts`:
- Around line 2383-2404: Guard the successful mount-result application in the
navigation flow with a `thisNav === navVersion` check before assigning
`unmountView`, `currentMountedIsland`, or `currentMountedPath`. If the
navigation is stale, discard the returned unmount function and do not update
mounted state, matching the existing guards in `_revalidate` and the initial
loader render.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 79acb320-dd5a-4833-8ad4-3d4e49390817

📥 Commits

Reviewing files that changed from the base of the PR and between e7a6791 and c55ba56.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • apps/website/package.json
  • packages/router/package.json
  • packages/router/src/index.test.ts
  • packages/router/src/index.ts
  • packages/store/src/index.test.ts
  • templates/elysia/package.json
  • templates/hono/package.json
  • templates/nitro/package.json
  • templates/vite/package.json

Comment on lines +1341 to +1345
const stop = persist(s, "p1b");
// Eager, non-reactive read right after persist() — no effect/subscribe involved.
expect(s.getState()).toEqual({ count: 42, label: "restored" });
stop();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guarantee persistence cleanup when the assertion fails.

If the assertion throws, stop() is skipped, leaving the subscription and storage event listener registered for subsequent tests. Wrap the assertion in try/finally.

Proposed fix
     const stop = persist(s, "p1b");
-    // Eager, non-reactive read right after persist() — no effect/subscribe involved.
-    expect(s.getState()).toEqual({ count: 42, label: "restored" });
-    stop();
+    try {
+      // Eager, non-reactive read right after persist() — no effect/subscribe involved.
+      expect(s.getState()).toEqual({ count: 42, label: "restored" });
+    } finally {
+      stop();
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const stop = persist(s, "p1b");
// Eager, non-reactive read right after persist() — no effect/subscribe involved.
expect(s.getState()).toEqual({ count: 42, label: "restored" });
stop();
});
const stop = persist(s, "p1b");
try {
// Eager, non-reactive read right after persist() — no effect/subscribe involved.
expect(s.getState()).toEqual({ count: 42, label: "restored" });
} finally {
stop();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/store/src/index.test.ts` around lines 1341 - 1345, Wrap the
assertion following persist(s, "p1b") in a try/finally block so stop() always
executes, including when expect(s.getState()) fails. Keep the existing state
expectation unchanged and place the cleanup call in the finally block.

@yzuyr
yzuyr merged commit ebe3897 into main Jul 17, 2026
5 checks passed
@yzuyr
yzuyr deleted the fix/router-fix-params-rerun branch July 17, 2026 15:34
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.

1 participant