Skip to content

fix(router): ensure layout and page loaders are merged correctly#67

Merged
yzuyr merged 1 commit into
mainfrom
fix/router-improve-loader-merging
Jul 8, 2026
Merged

fix(router): ensure layout and page loaders are merged correctly#67
yzuyr merged 1 commit into
mainfrom
fix/router-improve-loader-merging

Conversation

@yzuyr

@yzuyr yzuyr commented Jul 8, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Improved routing behavior so page and layout data are combined more reliably during rendering and hydration.
    • Nested layouts now preserve full loader data, including page-specific values, across server and client paths.
  • Bug Fixes

    • Fixed cases where some route data could be lost when layouts and pages both provided input.
    • Improved consistency for client-side navigation and SSR output when multiple loaders are involved.
  • Chores

    • Updated router package versions across the website and templates.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes wrapLayout's keyed k:page slot handling in the router package so that composed loader props (from root layouts, nested layouts, and pages) merge correctly across mount, hydration, and SSR paths without losing page-only keys. Extensive tests for composeLoaders behavior are added, and @ilha/router is bumped to 0.8.3 across the monorepo.

Changes

Router Loader Composition Fix

Layer / File(s) Summary
wrapLayout merged props handling
packages/router/src/index.ts
layoutHtmlWithEmptyKPage and wrapLayout now merge stored "layout merged input" props with per-call props via a new setLayoutMergedInput function, called from Wrapped.mount, Wrapped[ISLAND_MOUNT_INTERNAL], and Wrapped.hydratable.
Codegen composeLoaders test coverage
packages/router/src/codegen.test.ts
New tests assert composeLoaders chain ordering for server loaders across root/nested layouts and page, plus clientLoad detection for mixed client/server loader scenarios.
Runtime loader composition and hydration tests
packages/router/src/index.test.ts
New tests cover composeLoaders() merging with collision resolution, runLoader()/attachLoader() composed data, SSR hydratable serialization, client-side loader composition/precedence, and nested wrapLayout prop propagation to leaf components.

Router version bump propagation

Layer / File(s) Summary
Version bump and dependency updates
packages/router/package.json, apps/website/package.json, templates/elysia/package.json, templates/hono/package.json, templates/nitro/package.json, templates/vite/package.json
@ilha/router package version incremented from 0.8.2 to 0.8.3 and dependent package manifests updated to reference the new version.

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

Possibly related PRs

  • ilhajs/ilha#11: Introduces the page/layout loader and composeLoaders pipeline whose props this PR's wrapLayout fix propagates through the keyed page slot.
  • ilhajs/ilha#40: Also modifies wrapLayout's keyed k:page slot mount/hydration behavior in the same file.
  • ilhajs/ilha#51: Overlapping refactor of wrapLayout's nested k:page slot handling during hydratable/render composition, with corresponding tests in the same test file.
🚥 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 clearly matches the main change: fixing router loader merging between layouts and pages.
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-improve-loader-merging

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.

🧹 Nitpick comments (4)
packages/router/src/index.ts (2)

370-370: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove unnecessary ?? {} fallback in spread.

Same as line 335 — ...(props ?? {}) is equivalent to ...props since spreading undefined is a no-op. Flagged by unicorn(no-useless-fallback-in-spread).

♻️ Proposed fix
     const slotProps =
-      merged && typeof merged === "object" ? { ...merged, ...(props ?? {}) } : props;
+      merged && typeof merged === "object" ? { ...merged, ...props } : props;
🤖 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` at line 370, The spread fallback in the merge
logic is unnecessary because spreading undefined is already a no-op. Update the
object merge expression in the relevant router merge path (the one using merged
and props) to spread props directly instead of using the nullish-coalescing
fallback, keeping the same behavior while satisfying
unicorn(no-useless-fallback-in-spread).

Source: Linters/SAST tools


335-335: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove unnecessary ?? {} fallback in spread.

Spreading undefined in an object literal is a no-op, so ...(partial ?? {}) is equivalent to ...partial. The linter flagged this via unicorn(no-useless-fallback-in-spread).

♻️ Proposed fix
-  const shellChild = ((partial?: Record<string, unknown>) =>
-    rawKeyed({ ...props, ...(partial ?? {}) })) as unknown as Island<any, any>;
+  const shellChild = ((partial?: Record<string, unknown>) =>
+    rawKeyed({ ...props, ...partial })) as unknown as Island<any, any>;
🤖 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` at line 335, The spread in the router Island
creation path uses an unnecessary fallback: in the expression inside the
`rawKeyed`/`Island` construction, replace `...(partial ?? {})` with a direct
spread of `partial` since `undefined` is already a no-op in object spreads. Keep
the change localized to the `rawKeyed` call so the `Island<any, any>` cast and
surrounding props merging stay unchanged.

Source: Linters/SAST tools

packages/router/src/index.test.ts (2)

1397-1397: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unused input parameter in test island.

The Page island destructures input but doesn't use it — this test validates runLoader output, not rendered HTML. Eslint flagged this (no-unused-vars).

♻️ Proposed fix
-    const Page = ilha.render(({ input }: any) => `<p>ok</p>`);
+    const Page = ilha.render(() => `<p>ok</p>`);
🤖 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.test.ts` at line 1397, The test island in the Page
render callback destructures an input parameter that is never used, triggering
no-unused-vars. Update the ilha.render callback in index.test.ts to remove the
unused destructured input from the Page definition, keeping the test focused on
runLoader output rather than rendered HTML. Use the Page and ilha.render symbols
to locate the callback and ensure the render signature only includes parameters
that are actually referenced.

Source: Linters/SAST tools


1552-1553: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove duplicate .input<...>() calls — copy-paste artifacts.

Multiple test islands call .input<...>() two or three times redundantly. Each call returns a fresh IlhaBuilder, so only the last one takes effect. Since all calls use the same type parameter, the duplicates are no-ops but make the tests harder to read.

♻️ Example fix for lines 1552-1553
     const Page = ilha
-      .input<{ root: boolean; nested: boolean; page: boolean }>()
-      .input<{ root: boolean; nested: boolean; page: boolean }>()
       .input<{ root: boolean; nested: boolean; page: boolean }>()
       .render(({ input }) => `<p>${String(input.page)}</p>`);

Also applies to: 2755-2756, 2783-2784, 2791-2792

🤖 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.test.ts` around lines 1552 - 1553, The test islands
are calling ilha.input<...>() multiple times in a row, but each call creates a
new IlhaBuilder so only the last one is used. Remove the redundant duplicate
.input<...>() calls in the affected test setups (for example where Page is
defined) and keep a single input declaration per island so the intent is clear
and the tests remain unchanged.
🤖 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.

Nitpick comments:
In `@packages/router/src/index.test.ts`:
- Line 1397: The test island in the Page render callback destructures an input
parameter that is never used, triggering no-unused-vars. Update the ilha.render
callback in index.test.ts to remove the unused destructured input from the Page
definition, keeping the test focused on runLoader output rather than rendered
HTML. Use the Page and ilha.render symbols to locate the callback and ensure the
render signature only includes parameters that are actually referenced.
- Around line 1552-1553: The test islands are calling ilha.input<...>() multiple
times in a row, but each call creates a new IlhaBuilder so only the last one is
used. Remove the redundant duplicate .input<...>() calls in the affected test
setups (for example where Page is defined) and keep a single input declaration
per island so the intent is clear and the tests remain unchanged.

In `@packages/router/src/index.ts`:
- Line 370: The spread fallback in the merge logic is unnecessary because
spreading undefined is already a no-op. Update the object merge expression in
the relevant router merge path (the one using merged and props) to spread props
directly instead of using the nullish-coalescing fallback, keeping the same
behavior while satisfying unicorn(no-useless-fallback-in-spread).
- Line 335: The spread in the router Island creation path uses an unnecessary
fallback: in the expression inside the `rawKeyed`/`Island` construction, replace
`...(partial ?? {})` with a direct spread of `partial` since `undefined` is
already a no-op in object spreads. Keep the change localized to the `rawKeyed`
call so the `Island<any, any>` cast and surrounding props merging stay
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f47f42c5-61d0-4136-9e08-b49fef9a4e49

📥 Commits

Reviewing files that changed from the base of the PR and between c1584fb and edbb7f4.

⛔ 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/codegen.test.ts
  • packages/router/src/index.test.ts
  • packages/router/src/index.ts
  • templates/elysia/package.json
  • templates/hono/package.json
  • templates/nitro/package.json
  • templates/vite/package.json

@yzuyr
yzuyr merged commit 4359298 into main Jul 8, 2026
5 checks passed
@yzuyr
yzuyr deleted the fix/router-improve-loader-merging branch July 8, 2026 10:57
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