| name | expo-router-best-practices |
| description | creating new routes… |
Expo Router Best Practices
This skill provides guidance for implementing file-based routing with Expo Router following established best practices and official documentation patterns.
Core Principles
1. Routes Are Thin Wrappers
Route files in the app/ directory should be minimal pass-throughs to feature screen components. Business logic and complex UI components belong in feature directories, not route files.
import { Main } from "@/features/compare-players/screens/Main";
export default function CompareScreen() {
return <Main />;
}
export default function CompareScreen() {
const { playerId } = useLocalSearchParams();
const [data, setData] = useState(null);
return <ComplexUI />;
}
2. Descriptive Component Names
Use descriptive names for route components, not generic names.
export default function CompareScreen() { ... }
export default function PlayerDetailScreen() { ... }
export default function SettingsScreen() { ... }
export default function Screen() { ... }
export default function Page() { ... }
export default function Index() { ... }
3. Document Route URLs in JSDoc
Include the URL pattern in route file documentation.
export default function PlayerDetailScreen() {
return <Main />;
}
File Structure Patterns
Directory Organization
app/
├── _layout.tsx # Root layout (initialization, providers)
├── index.tsx # Default route (/)
├── +not-found.tsx # 404 handling
├── +html.tsx # Web HTML customization (optional)
├── (tabs)/ # Tab navigator group
│ ├── _layout.tsx # Tab configuration
│ ├── index.tsx # Default tab
│ ├── feed/ # Stack within tab
│ │ ├── _layout.tsx
│ │ ├── index.tsx
│ │ └── [postId].tsx
│ └── settings.tsx
├── (auth)/ # Auth screens group
│ ├── sign-in.tsx
│ └── create-account.tsx
└── modal.tsx # Modal route
Route Notation Reference
| Notation | Purpose | Example | URL |
|---|
file.tsx | Static route | about.tsx | /about |
[param].tsx | Dynamic route | [userId].tsx | /123 |
[...slug].tsx | Catch-all route | [...path].tsx | /a/b/c |
(group)/ | Route group (no URL impact) | (tabs)/ | / |
index.tsx | Default route | feed/index.tsx | /feed |
_layout.tsx | Layout definition | (tabs)/_layout.tsx | - |
+not-found.tsx | 404 handler | +not-found.tsx | - |
Layout Patterns
Root Layout
The root _layout.tsx replaces App.jsx/tsx. Place initialization code here.
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
});
useEffect(() => {
if (loaded) {
SplashScreen.hide();
}
}, [loaded]);
if (!loaded) {
return null;
}
return <Stack />;
}
Stack Layout
import { Stack } from "expo-router";
export const unstable_settings = {
initialRouteName: "index",
};
export default function ProductsLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: "Products" }} />
<Stack.Screen name="[productId]" options={{ headerShown: false }} />
</Stack>
);
}
Tab Layout
import { Tabs } from "expo-router";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
export default function TabLayout() {
return (
<Tabs screenOptions={{ headerShown: false }}>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color }) => (
<MaterialIcons size={28} name="home" color={color} />
),
}}
/>
<Tabs.Screen name="feed" options={{ title: "Feed" }} />
<Tabs.Screen name="settings" options={{ title: "Settings" }} />
</Tabs>
);
}
Protected Routes (SDK 53+)
import { Stack } from "expo-router";
import { useAuthState } from "@/hooks/useAuthState";
export default function RootLayout() {
const { isLoggedIn } = useAuthState();
return (
<Stack>
<Stack.Protected guard={isLoggedIn}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
</Stack.Protected>
<Stack.Protected guard={!isLoggedIn}>
<Stack.Screen name="sign-in" />
<Stack.Screen name="create-account" />
</Stack.Protected>
</Stack>
);
}
Navigation Patterns
Declarative Navigation (Preferred)
import { Link } from "expo-router";
<Link href="/about">About</Link>
<Link href="/profile" asChild>
<Pressable>
<Text>Profile</Text>
</Pressable>
</Link>
<Link href={{ pathname: "/user/[id]", params: { id: "123" } }}>
View User
</Link>
<Link href="/heavy-page" prefetch>Heavy Page</Link>
Imperative Navigation
import { useRouter } from "expo-router";
export default function Component() {
const router = useRouter();
const handleNavigate = () => {
router.navigate("/about");
router.push("/details");
router.replace("/home");
router.back();
router.navigate({
pathname: "/user/[id]",
params: { id: "123" },
});
};
return <Button onPress={handleNavigate} title="Navigate" />;
}
Defensive Navigation Guards
Always validate parameters before navigation to prevent broken URLs.
const handleNavigation = useCallback(() => {
if (!entityId) {
console.error("Cannot navigate: entity ID is missing");
return;
}
router.push(`/players/${entityId}`);
}, [entityId, router]);
Reading Route Parameters
import { useLocalSearchParams, useGlobalSearchParams } from "expo-router";
export default function UserPage() {
const { id, tab } = useLocalSearchParams<{ id: string; tab?: string }>();
const globalParams = useGlobalSearchParams();
return <Text>User ID: {id}</Text>;
}
Deep Linking
Configure URL Scheme
In app.json or app.config.js:
{
"expo": {
"scheme": "myapp"
}
}
Initial Route for Deep Links
Ensure proper back navigation when deep linking.
export const unstable_settings = {
initialRouteName: "index",
};
export default function FeedLayout() {
return <Stack />;
}
Deep Link with Anchor
<Link href="/feed/post/123" withAnchor>
View Post
</Link>
Common Patterns
Stacks Inside Tabs
app/
├── (tabs)/
│ ├── _layout.tsx # Tab navigator
│ ├── index.tsx # Home tab
│ ├── feed/ # Feed tab with stack
│ │ ├── _layout.tsx # Stack navigator
│ │ ├── index.tsx # Feed list
│ │ └── [postId].tsx # Post detail
│ └── settings.tsx # Settings tab
Shared Routes Between Tabs
app/
├── (tabs)/
│ ├── _layout.tsx
│ ├── (feed)/ # Feed tab group
│ │ └── index.tsx
│ ├── (search)/ # Search tab group
│ │ └── index.tsx
│ └── (feed,search)/ # Shared between both
│ └── users/
│ └── [userId].tsx
Modal Routes
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{
presentation: "modal",
animation: "slide_from_bottom",
}}
/>
</Stack>
Anti-Patterns to Avoid
1. Business Logic in Route Files
Route files should only import and render feature components.
2. Deeply Nested Navigators
Avoid nesting stacks within stacks unnecessarily. Use route groups instead.
3. Missing initialRouteName
Always set initialRouteName in stack layouts for proper deep link behavior.
4. Hardcoded Navigation Paths
Use typed routes or constants instead of string literals.
router.push("/players/123/compare");
router.push({
pathname: "/players/[playerId]/compare",
params: { playerId: "123" },
});
5. Using window APIs Without Platform Checks
const width = window.innerWidth;
import { useWindowDimensions } from "react-native";
const { width } = useWindowDimensions();
Resources
For detailed documentation on specific topics, refer to:
references/official-docs.md - Condensed official Expo Router documentation
scripts/generate-route.py - Route scaffolding script
Official Documentation: https://docs.expo.dev/router/introduction/