Ecopages0.2.0-rc.4

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

npm install @ecopages/react-router

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:

ValueDescription
morph(Default) a clean geometric morph. Prevents ghosting on shared elements.
fadeStandard 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.

  1. Initial Load: Server renders the full page with SSR.
  2. Hydration: React hydrates the page, router starts listening for clicks.
  3. 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:

AttributeDescription
data-eco-reloadForce 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 download attribute
  • Link points to a different origin
  • Link starts with # or javascript:

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
FrameworkReact onlyMPA (KitaJS, Lit, vanilla)
DOM StrategyReact reconciliationmorphdom diffing
Layout PersistenceVia React component treeVia data-eco-persist
View TransitionsSupportedSupported
State PreservationReact state in layoutDOM element state

License

MIT