---
title: 'React Router'
description: 'Enable SPA navigation for React apps using the React Router integration.'
order: 6
---

import { RuiAlert, RuiAlertDescription, RuiAlertTitle } from '@ecopages/radiant-ui/alert';
import { CodeTabs } from '@/components/code-tabs';

# 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.

<RuiAlert variant="info" layout="banner" class="unstyled">
	<RuiAlertTitle>React Only</RuiAlertTitle>
	<RuiAlertDescription>
		<p>
			This package is specifically designed for React applications. For MPA-style rendering (KitaJS, Lit, vanilla
			JS), use <a href="/docs/ecosystem/browser-router">@ecopages/browser-router</a> instead.
		</p>
	</RuiAlertDescription>
</RuiAlert>

## 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

<CodeTabs
	name="react-router-install"
	tabs={[
		{ id: 'npm', label: 'npm', code: 'npm install @ecopages/react-router' },
		{ id: 'pnpm', label: 'pnpm', code: 'pnpm add @ecopages/react-router' },
		{ id: 'bun', label: 'bun', code: 'bun add @ecopages/react-router' },
	]}
	defaultSelectedKey="npm"
/>

## Setup

### 1. Configure the React Plugin

Add the router adapter to your `eco.config.ts`:

```typescript
import { defineConfig } from '@ecopages/core/config';
import { reactPlugin } from '@ecopages/react';
import { ecoRouter } from '@ecopages/react-router';

export default defineConfig({
	rootDir: import.meta.dirname,
	integrations: [reactPlugin({ router: ecoRouter() })],
});
```

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.

```tsx
// 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.identity.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](/docs/integrations/react#shared-runtime-vendors).

#### Nested layouts

Declare an outer → inner stack when routes need multiple wrappers — for example a site shell plus a section sidebar:

```tsx
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](/docs/core/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:

```tsx
<div data-view-transition="hero" data-view-transition-animate="fade">
```

#### Duration

Control the speed of a specific transition:

```tsx
<div data-view-transition="hero" data-view-transition-duration="500ms">
```

### 3. Use Standard Links

```tsx
// 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.

```typescript
import { ecoRouter } from '@ecopages/react-router';

reactPlugin({ router: ecoRouter() });
```

### `useRouter()`

Hook for programmatic navigation.

```tsx
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 `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:**

```typescript
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
