---
name: expo-router
description: >
Expo Router file-based routing for Universal React Native applications.
Trigger: When working with Expo Router navigation, file-based routes, layouts, tabs, modals, typed routes, or understanding how routing works in Liftera mobile app.
license: Apache-2.0
metadata:
author: liftera
version: "1.0"
scope: [root, mobile]
auto_invoke: "Working with Expo Router navigation"
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
---
## What is Expo Router?
Expo Router is a **file-based router** for Universal React Native applications. It brings the best file-system routing concepts from the web to native applications, allowing your routing to work seamlessly across Android, iOS, and web platforms.
### Core Principles
1. **File-based routing**: When a file is added to the `app/` directory, it automatically becomes a route
2. **Universal**: Same navigation structure works on Android, iOS, and web
3. **Built on React Navigation**: Provides native platform-optimized navigation
4. **Deep linkable**: Every screen is automatically shareable with URLs
5. **Offline-first**: Routes are cached and run without network connection
6. **Type-safe**: Full TypeScript support with auto-generated typed routes
---
## File Notation System
Expo Router uses special file naming conventions to define routing behavior:
### 1. Static Routes (No Notation)
```
app/
├── home.tsx # /home
├── feed/
│ └── favorites.tsx # /feed/favorites
```
Regular file and directory names create **static routes**. The URL matches exactly as it appears in the file tree.
### 2. Dynamic Routes (Square Brackets)
```
app/
├── [userName].tsx # /evanbacon, /expo, etc.
├── products/
│ └── [productId]/
│ └── index.tsx # /products/123
```
Square brackets `[param]` create **dynamic segments**. The parameter is accessible via `useLocalSearchParams()` hook:
```typescript
// app/[userName].tsx
import { useLocalSearchParams } from "expo-router";
export default function UserProfile() {
const { userName } = useLocalSearchParams<{ userName: string }>();
// userName = "evanbacon" when visiting /evanbacon
}
```
### 3. Route Groups (Parentheses)
```
app/
├── (tabs)/
│ ├── index.tsx # / (not /tabs)
│ └── settings.tsx # /settings (not /tabs/settings)
├── (auth)/
│ ├── login.tsx # /login (not /auth/login)
│ └── signup.tsx # /signup
```
Parentheses `(group)` create **route groups** that organize files without affecting the URL structure. Critical for:
- Organizing related routes
- Sharing layouts between routes
- Creating multiple navigation contexts (tabs, stacks)
### 4. Index Routes
```
app/
├── index.tsx # / (root)
├── profile/
│ └── index.tsx # /profile
```
`index.tsx` files define the **default route** for a directory. Like `index.html` on the web.
### 5. Layout Routes (\_layout.tsx)
```
app/
├── _layout.tsx # Root layout (wraps everything)
├── (tabs)/
│ ├── _layout.tsx # Tab navigator layout
│ └── index.tsx
```
`_layout.tsx` files are **not routes themselves** but define how routes inside their directory relate to each other:
- Define navigation structure (Stack, Tabs, Drawer)
- Wrap child routes with providers
- Configure screen options
- Rendered **before** child routes
**Critical**: The root `app/_layout.tsx` is where initialization code goes (previously in `App.jsx`).
### 6. Special Routes (Plus Sign)
```
app/
├── +not-found.tsx # 404 handler
├── +html.tsx # Web HTML customization
├── +native-intent.tsx # Deep link handler
├── +middleware.ts # Request middleware
```
Routes with `+` prefix have special meaning:
- `+not-found`: Catches unmatched routes (404)
- `+html`: Customizes HTML boilerplate on web
- `+native-intent`: Handles third-party deep links
- `+middleware`: Runs code before route renders
---
## Navigation Structures
### Stack Navigator
A **stack** is the foundational navigation pattern. Routes animate on top of each other:
- **Android**: Slides up from bottom
- **iOS**: Slides in from right
- **Web**: Browser-like navigation
```typescript
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function Layout() {
return (
);
}
```
**How it works**: Each `` corresponds to a file in the same directory. The `name` prop matches the filename (without extension).
### Tab Navigator
**Tabs** create a persistent bottom navigation bar (iOS/Android) or top tabs:
```typescript
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
export default function TabLayout() {
return (
}}
/>
}}
/>
);
}
```
**File structure**:
```
app/
├── _layout.tsx # Root stack
└── (tabs)/ # Route group (no URL segment)
├── _layout.tsx # Tab navigator
├── index.tsx # First tab (/)
└── profile.tsx # Second tab (/profile)
```
**Why route groups?** The `(tabs)` group prevents `/tabs/profile` and creates clean URLs like `/profile`.
### Nested Navigation
Layouts can be nested to create complex navigation hierarchies:
```
app/
├── _layout.tsx # Root Stack
└── (tabs)/ # Route group
├── _layout.tsx # Tabs (Home, Feed, Profile)
├── index.tsx # Home tab
├── feed/
│ ├── _layout.tsx # Stack inside Feed tab
│ ├── index.tsx # Feed list
│ └── [id].tsx # Feed detail (pushed on stack)
└── profile.tsx # Profile tab
```
**Navigation flow**:
1. Root layout renders Tabs
2. Tabs render Home/Feed/Profile
3. Feed tab has its own Stack for list → detail navigation
---
## Navigation APIs
### Imperative Navigation
```typescript
import { router } from "expo-router";
// Push new route on stack
router.push("/profile");
router.push({ pathname: "/user/[id]", params: { id: "123" } });
// Replace current route (no back button)
router.replace("/login");
// Go back
router.back();
// Navigate to specific route in stack
router.navigate("/settings");
// Dismiss modal/stack
router.dismiss();
router.dismissAll();
```
### Declarative Navigation
```typescript
import { Link } from 'expo-router';
// Static route
Go to Profile
// Dynamic route
View User
// Replace instead of push
Login
```
### Navigation State Hooks
```typescript
import { usePathname, useSegments, useLocalSearchParams } from "expo-router";
// Current path
const pathname = usePathname(); // "/feed/123"
// Path segments
const segments = useSegments(); // ["feed", "123"]
// Route parameters
const { id } = useLocalSearchParams<{ id: string }>();
```
---
## Typed Routes (Liftera Enabled)
Liftera has `experiments.typedRoutes: true` in `app.json`, enabling **compile-time route validation**.
### How It Works
1. Expo CLI scans `app/` directory
2. Generates `.expo/types/router.d.ts` with all valid routes
3. TypeScript validates `href` props at compile time
### Type-Safe Navigation
```typescript
import { router, Link } from 'expo-router';
// ✅ Valid routes - TypeScript happy
// ❌ TypeScript errors - route doesn't exist
// Typo caught at compile time
// ❌ TypeScript errors - missing required params
// Must use object form with params
// ❌ TypeScript errors - invalid params
// Param name must be 'id', not 'userId'
```
### Benefits
- **Catch typos** before runtime
- **Autocomplete** for all routes
- **Refactor safely** - rename files, TypeScript finds all usages
- **Parameter validation** - ensure correct param names
---
## Modals
Modals are **routes presented differently**, not separate components.
```typescript
// app/_layout.tsx
```
**Navigate to modal**:
```typescript
router.push("/modal"); // Presents as modal
router.back(); // Dismisses modal
```
**Deep linking**: Modals work with deep links. Opening `myapp://modal` presents the modal correctly.
---
## Deep Linking & Universal Links
Every route is automatically deep linkable:
```json
// app.json
{
"expo": {
"scheme": "liftera"
}
}
```
**URL mapping**:
- `liftera://` → `/` (index)
- `liftera://profile` → `/profile`
- `liftera://user/123` → `/user/[id]` with `id: "123"`
**Web URLs** (when deployed):
- `https://liftera.app/` → `/`
- `https://liftera.app/profile` → `/profile`
**Same code, all platforms** - no platform-specific linking logic needed.
---
## Critical Patterns
### Protected Routes
```typescript
// app/_layout.tsx
import { useAuth } from '@/hooks/useAuth';
import { Redirect, Slot } from 'expo-router';
export default function RootLayout() {
const { user, loading } = useAuth();
if (loading) return ;
if (!user) return ;
return ;
}
```
**How it works**: Root layout checks auth state. Unauthenticated users are redirected before any child routes render.
### Shared Layouts
```typescript
// app/(app)/_layout.tsx - Authenticated app
// app/(auth)/_layout.tsx - Auth flow
```
Route groups let you define different layout contexts for different parts of your app.
### Error Boundaries
```typescript
// app/_layout.tsx
export { ErrorBoundary } from 'expo-router';
// Or custom
export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
return (
Error: {error.message}
);
}
```
**Automatic error handling**: Errors in routes are caught and displayed without crashing the app.
---
## Critical Rules
### File Naming
- **ALWAYS** use `.tsx` extension for TypeScript routes
- **NEVER** use `.js` - Liftera is TypeScript-first
- **ALWAYS** use lowercase for route files (convention)
### Layouts
- **ALWAYS** define `_layout.tsx` when using Stack/Tabs
- **NEVER** skip root `app/_layout.tsx` - it's required
- **ALWAYS** render `` in layouts without navigators
### Navigation
- **ALWAYS** use typed routes with `useLocalSearchParams()`
- **NEVER** hardcode route strings - let TypeScript validate
- **ALWAYS** use object form for dynamic routes with params
- **NEVER** import React Navigation directly - use Expo Router APIs
### Route Groups
- **ALWAYS** use `(group)` for organizing without URL impact
- **NEVER** nest route groups unnecessarily
- **ALWAYS** use route groups for tabs/auth flows
---
## Resources
- **Expo Router Docs**: https://docs.expo.dev/router/introduction/
- **File Notation**: https://docs.expo.dev/router/basics/notation/
- **Typed Routes**: https://docs.expo.dev/router/reference/typed-routes/
- **Stack Navigator**: https://docs.expo.dev/router/advanced/stack/
- **Tabs Navigator**: https://docs.expo.dev/router/advanced/tabs/
- **Modals**: https://docs.expo.dev/router/advanced/modals/