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
bun add @ecopages/react-routerSetup
1. Configure the React Plugin
Add the router adapter to your eco.config.ts:
import { ConfigBuilder } from '@ecopages/core';
import { reactPlugin } from '@ecopages/react';
import { ecoRouter } from '@ecopages/react-router';
const config = await new ConfigBuilder()
.setRootDir(import.meta.dir)
.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), use config.layout:
// src/layouts/base-layout.tsx
export const BaseLayout = ({ children }) => (
<html>
<head>...</head>
<body>
<header>My Site</header>
<main>{children}</main>
<footer>...</footer>
</body>
</html>
);
// src/pages/index.tsx
import { BaseLayout } from '../layouts/base-layout';
import { eco } from '@ecopages/core';
const HomePage = eco.page({
layout: BaseLayout,
render: () => <h1>Welcome</h1>
});
export default HomePage;When navigating between pages with the same layout, only the page content updates - the layout stays mounted.
View Transitions
Default Behavior: The router automatically optimizes shared element transitions to "morph" the geometry without ghosting (no cross-fade).
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, outputName, externals };
importMapKey: string;
components: { router, pageContent };
getRouterProps(page, props): 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 | Not yet supported | Supported |
| State Preservation | React state in layout | DOM element state |
License
MIT