Skip to content

ui-next: layout refactor & dynamic template#1182

Merged
undefined-moe merged 2 commits into
hydro-dev:masterfrom
renbaoshuo:ui-next-page-template
Jun 20, 2026
Merged

ui-next: layout refactor & dynamic template#1182
undefined-moe merged 2 commits into
hydro-dev:masterfrom
renbaoshuo:ui-next-page-template

Conversation

@renbaoshuo

@renbaoshuo renbaoshuo commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added end-to-end page template metadata propagation for bootstrapping and navigation.
    • Introduced a layout registration API so layouts can be associated with pages.
    • Enhanced page rendering to support template-based slot selection with sensible fallbacks, including a “Page not found” message.
  • Bug Fixes
    • Template header is now set and captured during the request/navigation flow, ensuring template data stays in sync.
  • Refactor
    • Default layout slot naming was updated; the homepage no longer exports a layout helper.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 665eca23-88ec-48b6-b0f0-d670ea82f926

📥 Commits

Reviewing files that changed from the base of the PR and between a41896c and 35e3e34.

📒 Files selected for processing (1)
  • framework/framework/base.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • framework/framework/base.ts

Walkthrough

The PR introduces template-aware layout routing in the ui-next frontend. The server middleware now emits an x-hydro-template response header derived from response.template. Both DEV and production HTML renderers inject the template value into the bootstrapping JSON payload. The PageData interface gains a required template field, populated from injection data on initial load and from the x-hydro-template header during client-side navigation via RouterProvider. The registry type system is extended with a SlotValue<N> conditional type mapping page/layout/component slot names to PageEntry/LayoutComponent/React.FC, and the store's setDefault/getDefault are made generic over this type. A new registerLayout function stores layout components under layout:${name} keys. registerPage is refactored to store a PageEntry with a layout reference instead of eagerly wrapping the layout at lazy-load time. The App slot now resolves the appropriate page entry using template and error state, then selects the matching layout component for rendering.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • hydro-dev/Hydro#1138: Extends the x-hydro-inject middleware header injection in framework/framework/base.ts by adding x-hydro-template propagation, directly related to this PR's server-side template header handling.
  • hydro-dev/Hydro#1159: Modifies packages/ui-next/src/app.tsx slot selection and layout rendering logic that this PR extends with template-driven entry and layout resolution.

Suggested reviewers

  • pandadtdyy
  • undefined-moe
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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 two main changes: a layout system refactor (moving from page-level layout exports to a centralized layout registry) and dynamic template support (introducing template tracking through headers and injection).
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

framework/framework/base.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.


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 and usage tips.

@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/ui-next/src/app.tsx (1)

13-40: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Store reads inside useMemo become stale when the store updates.

The useMemo at lines 13-33 reads from store.getDefault() multiple times, but its dependencies are [name, template, isError]. When a slot is registered late (e.g., via lazy plugin loading):

  1. useSyncExternalStore triggers a re-render because the store version changed
  2. useMemo returns the cached [slotName, entry] because its deps are unchanged
  3. entry remains stale (e.g., still undefined), causing "Page not found" even though the page is now registered

Consider restructuring to separate deterministic slot-name computation from store reads:

Suggested approach
- const [slotName, entry] = useMemo(() => {
-   if (isError) {
-     return ['page:error', store.getDefault('page:error')] as const;
-   }
-   const templateName = typeof template === 'string' ? template.replace(/\.html$/, '') : null;
-   if (templateName) {
-     const templateSlot = `page:${templateName}` as `page:${string}`;
-     const templateEntry = store.getDefault(templateSlot);
-     if (templateEntry) {
-       if (import.meta.env.DEV) {
-         console.log(`[ui-next] using template "${templateName}" for page "${name}"`);
-       }
-       return [templateSlot, templateEntry] as const;
-     }
-   }
-   const slot = `page:${name}` as `page:${string}`;
-   if (import.meta.env.DEV) {
-     console.log(`[ui-next] using page slot "${slot}"`);
-   }
-   return [slot, store.getDefault(slot)] as const;
- }, [name, template, isError]);
-
- const [subscribe, getSnapshot] = useMemo(() => [
-   (cb: () => void) => store.subscribe(slotName, cb),
-   () => store.getVersion(slotName),
- ], [slotName]);
-
- useSyncExternalStore(subscribe, getSnapshot);
+ // Step 1: Compute potential slot names (deterministic)
+ const candidateSlots = useMemo(() => {
+   if (isError) return ['page:error'] as const;
+   const templateName = typeof template === 'string' ? template.replace(/\.html$/, '') : null;
+   if (templateName) return [`page:${templateName}`, `page:${name}`] as const;
+   return [`page:${name}`] as const;
+ }, [name, template, isError]);
+
+ // Step 2: Subscribe to all candidate slots
+ const [subscribe, getSnapshot] = useMemo(() => [
+   (cb: () => void) => {
+     const unsubs = candidateSlots.map((s) => store.subscribe(s, cb));
+     return () => unsubs.forEach((u) => u());
+   },
+   () => candidateSlots.map((s) => store.getVersion(s)).join(','),
+ ], [candidateSlots]);
+
+ useSyncExternalStore(subscribe, getSnapshot);
+
+ // Step 3: Resolve entry fresh on each render (after subscription ensures re-render on changes)
+ const [slotName, entry] = (() => {
+   for (const slot of candidateSlots) {
+     const e = store.getDefault(slot);
+     if (e) return [slot, e] as const;
+   }
+   return [candidateSlots[candidateSlots.length - 1], undefined] as const;
+ })();
🤖 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/ui-next/src/app.tsx` around lines 13 - 40, The useMemo hook is
caching store reads from store.getDefault() based only on [name, template,
isError] dependencies, which causes stale entry values when the store is updated
asynchronously (e.g., via lazy plugin loading). Refactor by separating the
deterministic slot-name computation from the store reads: keep the slotName
calculation in useMemo with its current dependencies, but move the
store.getDefault(slotName) calls outside of useMemo so they re-execute whenever
slotName changes and when useSyncExternalStore detects store updates. This
ensures entry is always in sync with the current store state for the computed
slot.
🧹 Nitpick comments (1)
packages/ui-next/src/components/layout.tsx (1)

3-5: ⚡ Quick win

Avoid unconditional logging in the default layout render path.

This runs on every render and will add noisy console output in production.

♻️ Proposed change
 const Layout = defineSlot('layout:default', ({ children }: React.PropsWithChildren) => {
-  console.log('[ui-next] using default layout');
   return <>{children}</>;
 });
🤖 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/ui-next/src/components/layout.tsx` around lines 3 - 5, The Layout
component defined through defineSlot with the key 'layout:default' contains an
unconditional console.log statement that executes on every render, creating
unnecessary console noise in production. Remove the console.log statement
entirely from the component's render path, or if the logging is needed for
debugging purposes, wrap it with a development environment check (such as
checking process.env.NODE_ENV) so it only logs during development and not in
production builds.
🤖 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/ui-next/index.ts`:
- Line 194: The injected template values at line 194 and line 228 in the file
are inconsistent with how navigation normalizes templates in framework/base.ts.
Both instances currently inject context.handler.response.template verbatim, but
the navigation header contract strips the .html extension from the template
name. Normalize both injections by applying the same .html stripping logic that
the navigation code uses, ensuring PageData.template values remain consistent
between first render and client navigation to prevent resolving different page
layouts for the same route.

---

Outside diff comments:
In `@packages/ui-next/src/app.tsx`:
- Around line 13-40: The useMemo hook is caching store reads from
store.getDefault() based only on [name, template, isError] dependencies, which
causes stale entry values when the store is updated asynchronously (e.g., via
lazy plugin loading). Refactor by separating the deterministic slot-name
computation from the store reads: keep the slotName calculation in useMemo with
its current dependencies, but move the store.getDefault(slotName) calls outside
of useMemo so they re-execute whenever slotName changes and when
useSyncExternalStore detects store updates. This ensures entry is always in sync
with the current store state for the computed slot.

---

Nitpick comments:
In `@packages/ui-next/src/components/layout.tsx`:
- Around line 3-5: The Layout component defined through defineSlot with the key
'layout:default' contains an unconditional console.log statement that executes
on every render, creating unnecessary console noise in production. Remove the
console.log statement entirely from the component's render path, or if the
logging is needed for debugging purposes, wrap it with a development environment
check (such as checking process.env.NODE_ENV) so it only logs during development
and not in production builds.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7d89c169-6a3f-4a1b-b955-aded36273d22

📥 Commits

Reviewing files that changed from the base of the PR and between 43fb00e and a41896c.

📒 Files selected for processing (15)
  • framework/framework/base.ts
  • packages/ui-next/index.ts
  • packages/ui-next/src/app.tsx
  • packages/ui-next/src/components/layout.tsx
  • packages/ui-next/src/context/page-data.tsx
  • packages/ui-next/src/context/router.tsx
  • packages/ui-next/src/globals.ts
  • packages/ui-next/src/pages/homepage.tsx
  • packages/ui-next/src/registry/index.ts
  • packages/ui-next/src/registry/layout.ts
  • packages/ui-next/src/registry/page.tsx
  • packages/ui-next/src/registry/plugin.ts
  • packages/ui-next/src/registry/slot.tsx
  • packages/ui-next/src/registry/store.ts
  • packages/ui-next/src/registry/types.ts
💤 Files with no reviewable changes (1)
  • packages/ui-next/src/pages/homepage.tsx

Comment thread packages/ui-next/index.ts
const serialized = JSON.stringify({
HYDRO_INJECTED: true,
name: context.handler.context._matchedRouteName,
template: context.handler.response.template || '',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize injected template to match navigation header contract.

Line 194 and Line 228 inject context.handler.response.template verbatim, but navigation reads x-hydro-template from framework/framework/base.ts Line 87 where .html is stripped. This creates inconsistent PageData.template values between first render and client navigation, which can resolve different page:* entries/layouts for the same route.

Proposed fix
-                    template: context.handler.response.template || '',
+                    template: (context.handler.response.template || '').replace(/\.html$/, ''),
...
-                    template: context.handler.response.template || '',
+                    template: (context.handler.response.template || '').replace(/\.html$/, ''),

Also applies to: 228-228

🤖 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/ui-next/index.ts` at line 194, The injected template values at line
194 and line 228 in the file are inconsistent with how navigation normalizes
templates in framework/base.ts. Both instances currently inject
context.handler.response.template verbatim, but the navigation header contract
strips the .html extension from the template name. Normalize both injections by
applying the same .html stripping logic that the navigation code uses, ensuring
PageData.template values remain consistent between first render and client
navigation to prevent resolving different page layouts for the same route.

@undefined-moe undefined-moe merged commit 5d5a168 into hydro-dev:master Jun 20, 2026
6 checks passed
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.

2 participants