React Router
The @ecopages/react-router package enables Single Page Application (SPA) navigation for EcoPages React applications. It provides seamless client-side navigation while preserving full SSR benefits.
Features
- Opt-in SPA navigation - Configure once, works on all pages
- SSR preserved - Full server-side rendering on initial load
- Layout persistence - Layouts stay mounted during navigation
- Standard links - Works with regular anchor tags, no special components needed
- Head synchronization - Automatically updates title, meta, and stylesheets
- Pluggable architecture - Extensible adapter pattern for custom routers
Installation
Setup
1. Configure the React Plugin
Add the router adapter to your eco.config.ts:
import { ConfigBuilder } from '@ecopages/core/config-builder';
import { reactPlugin } from '@ecopages/react';
import { ecoRouter } from '@ecopages/react-router';
const appRoot = process.cwd();
const config = await new ConfigBuilder()
.setRootDir(appRoot)
.setIntegrations([reactPlugin({ router: ecoRouter() })])
.build();
export default config;That's it, all pages now have SPA navigation enabled.
2. Use Layouts (Optional)
For persistent UI across navigations (headers, sidebars), assign a declared layout on eco.page(). Use eco.layout() — plain React functions are not valid layout values.
// src/layouts/base-layout.tsx
import { eco } from '@ecopages/core';
import type { ReactNode } from 'react';
export const BaseLayout = eco.layout<{ children: ReactNode }>({
render: ({ children }) => (
<>
<header>My Site</header>
<main>{children}</main>
</>
),
});
// src/pages/index.tsx
import { BaseLayout } from '../layouts/base-layout';
import { eco } from '@ecopages/core';
export default eco.page({
layout: BaseLayout,
render: () => <h1>Welcome</h1>,
});When navigating between pages that share the same layout key (config.__eco.file or id), persistLayouts (enabled by default with ecoRouter()) keeps the layout mounted. Only page content updates.
Shared npm vendors: persisted layouts also require shared browser copies of provider libraries (TanStack Query, MobX, Redux, audio engines). Set runtimeProvider: true on provider root layouts and configure runtimeModules when page code imports those packages outside the layout graph. See Shared runtime vendors.
Nested layouts
Declare an outer → inner stack when routes need multiple wrappers — for example a site shell plus a section sidebar:
export default eco.page({
layout: [AppShell, DocsSection],
render: () => <h1>Docs</h1>,
});Routes with different inner tiers but the same outer layout — [AppShell, Docs] and [AppShell, Settings] — share one mounted AppShell during SPA navigation. React state in the shell survives; inner tiers mount and unmount with the active page.
See Creating Layouts for per-tier props, dependency rules, and SSR locals behavior.
View Transitions
Default Behavior: When enabled, the router opts the document out of the UA root group (no lighter flash on dark UIs). startViewTransition runs only for data-view-transition shared-element morphs, not full-page crossfades. Set viewTransitions: false to disable.
Visual check: dark background → SPA navigate → no lighter flash with defaults.
Directives
Control the animation style with data-view-transition-animate:
| Value | Description |
|---|---|
morph | (Default) a clean geometric morph. Prevents ghosting on shared elements. |
fade | Standard cross-fade animation. Use this to opt-out of the automatic fix. |
Example:
<div data-view-transition="hero" data-view-transition-animate="fade">Duration
Control the speed of a specific transition:
<div data-view-transition="hero" data-view-transition-duration="500ms">3. Use Standard Links
// These links are automatically intercepted for SPA navigation
<a href="/about">About</a>
<a href="/blog">Blog</a>
// Force full page reload with data-eco-reload
<a href="/external" data-eco-reload>External Link</a>How It Works
The router uses an HTML-First navigation strategy. This simplifies the architecture by treating the server-rendered HTML as the source of truth for both code splitting and data.
- Initial Load: Server renders the full page with SSR.
- Hydration: React hydrates the page, router starts listening for clicks.
- Navigation: When a link is clicked:
- Fetch: Router fetches the target page HTML.
- Prep: Extracts component URL and props; imports the new component.
- Transition:
- Captures "Old" state (Screenshot).
- React renders "New" state (DOM update).
- Browser animates from Old to New.
┌─────────────────────────────────────────────────────┐
│ Browser View Transition │
│ │
│ 1. Capture Old State (Screenshot) │
│ ↓ │
│ 2. React State Update │
│ (Mount New Page Component) │
│ ↓ │
│ 3. Wait for Render │
│ ↓ │
│ 4. Animate (Old → New) │
└─────────────────────────────────────────────────────┘
API
ecoRouter()
Factory function that creates a router adapter for the React plugin.
import { ecoRouter } from '@ecopages/react-router';
reactPlugin({ router: ecoRouter() });useRouter()
Hook for programmatic navigation.
import { useRouter } from '@ecopages/react-router';
const MyComponent = () => {
const { navigate, isPending } = useRouter();
const handleClick = () => {
navigate('/about');
};
return (
<button onClick={handleClick} disabled={isPending}>
{isPending ? 'Loading...' : 'Go to About'}
</button>
);
};Router Options
The router respects these HTML attributes:
| Attribute | Description |
|---|---|
data-eco-reload | Force a full page reload instead of SPA navigation |
target="_blank" | Opens in new tab (not intercepted) |
Links are automatically skipped when:
- Modifier keys are held (Ctrl, Cmd, Shift, Alt)
- Link has
downloadattribute - Link points to a different origin
- Link starts with
#orjavascript:
Architecture
The router uses a pluggable adapter pattern that separates concerns:
ReactRouterAdapter Interface:
interface ReactRouterAdapter {
name: string;
bundle: { importPath: string; outputName: string; externals: string[] };
components: { router: string; pageContent: string };
getRouterProps(page: string, props: string): string;
}This allows for alternative router implementations while keeping the core integration simple.
Comparison with Browser Router
| Feature | @ecopages/react-router | @ecopages/browser-router |
|---|---|---|
| Framework | React only | MPA (KitaJS, Lit, vanilla) |
| DOM Strategy | React reconciliation | morphdom diffing |
| Layout Persistence | Via React component tree | Via data-eco-persist |
| View Transitions | Supported | Supported |
| State Preservation | React state in layout | DOM element state |
License
MIT