Expo Router vs React Navigation vs Solito: React Native Routing 2026
Expo Router vs React Navigation vs Solito: React Native Routing 2026
TL;DR
Navigation is one of the most consequential choices in a React Native app. React Navigation v7 is the established standard — 25k+ GitHub stars, comprehensive ecosystem (stack, tab, drawer, material), and works everywhere React Native runs. Expo Router is the file-based evolution — built on React Navigation but adds file-system routing (like Next.js), URL-based deep linking, universal API routes, and first-class web support. Solito bridges the gap — a lightweight library that unifies React Navigation (native) and Next.js App Router (web) in a single shared codebase. For Expo-only apps: Expo Router. For maximum ecosystem flexibility: React Navigation. For true Next.js + Expo universal apps: Solito.
Key Takeaways
- React Navigation v7 GitHub stars: ~25k — the most battle-tested React Native navigation library
- Expo Router is file-based — routes defined by file structure like
app/(tabs)/index.tsx - Expo Router added API Routes in SDK 51 — server-side endpoints alongside your mobile app
- Solito enables true code sharing — the same
<Link>anduseRouter()work in both Next.js and Expo - React Navigation v7 uses static configuration — improved TypeScript inference over v6
- All three support deep linking — Expo Router does it automatically via file structure
- Expo Router v4 (SDK 52) made web first-class — server components and streaming work on web
The React Native Navigation Problem
React Native navigation is inherently more complex than web routing:
- Native stack transitions — push/pop animations must feel native (not web-like)
- Tab bars — bottom tabs with separate navigation stacks per tab
- Deep links — URL
myapp://users/123must navigate to the right screen - Universal routing — same code working on iOS, Android, AND web
- TypeScript safety — knowing what params each screen expects
React Navigation v7: The Established Standard
React Navigation is the canonical navigation solution for React Native. Version 7 (2024) added static configuration, improved TypeScript inference, and React Native's new architecture support.
Installation
npm install @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs
npx expo install react-native-screens react-native-safe-area-context
Static Configuration (v7 New Feature)
// v7: Static config for better TypeScript inference — no useNavigation() type casting
import { createStaticNavigation } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
// Define screen params inline
const RootStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
Profile: {
screen: ProfileScreen,
// TypeScript knows these params exist
linking: {
path: "users/:userId",
parse: { userId: Number },
},
},
Settings: SettingsScreen,
},
});
const TabNavigator = createBottomTabNavigator({
screens: {
Feed: FeedTab,
Search: SearchTab,
Inbox: InboxTab,
},
});
// Root navigator
const Navigation = createStaticNavigation(RootStack);
export default function App() {
return <Navigation />;
}
Traditional Dynamic Navigation
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
type RootStackParamList = {
MainTabs: undefined;
UserProfile: { userId: string; name: string };
Settings: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
const Tab = createBottomTabNavigator();
function MainTabs() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
const icons: Record<string, string> = {
Feed: focused ? "🏠" : "⌂",
Search: "🔍",
Profile: "👤",
};
return <Text style={{ fontSize: size }}>{icons[route.name]}</Text>;
},
})}
>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="MainTabs" component={MainTabs} options={{ headerShown: false }} />
<Stack.Screen
name="UserProfile"
component={UserProfileScreen}
options={({ route }) => ({ title: route.params.name })}
/>
<Stack.Screen name="Settings" component={SettingsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
Typed Navigation Hooks
// Fully typed navigation with React Navigation v7
import { useNavigation, useRoute } from "@react-navigation/native";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { RouteProp } from "@react-navigation/native";
type UserProfileProps = {
navigation: NativeStackNavigationProp<RootStackParamList, "UserProfile">;
route: RouteProp<RootStackParamList, "UserProfile">;
};
export function UserProfileScreen({ navigation, route }: UserProfileProps) {
const { userId, name } = route.params;
return (
<View>
<Text>Profile: {name} (ID: {userId})</Text>
<Button title="Go Back" onPress={() => navigation.goBack()} />
<Button
title="Settings"
onPress={() => navigation.navigate("Settings")}
/>
</View>
);
}
Expo Router: File-Based Navigation
Expo Router creates your navigation from the file system. Create a file, get a route. No separate navigation config needed.
Installation
npx create-expo-app MyApp --template tabs
# Or add to existing Expo project
npx expo install expo-router
File Structure → Routes
app/
├── _layout.tsx → Root layout (NavigationContainer)
├── index.tsx → "/" (home)
├── (tabs)/ → Tab group (no URL segment)
│ ├── _layout.tsx → Tab navigator config
│ ├── index.tsx → "/feed" tab
│ ├── search.tsx → "/search" tab
│ └── profile.tsx → "/profile" tab
├── users/
│ ├── [id].tsx → "/users/:id" dynamic route
│ └── index.tsx → "/users"
├── settings.tsx → "/settings"
└── +not-found.tsx → 404 page
Root Layout
// app/_layout.tsx
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
export default function RootLayout() {
return (
<>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="users/[id]" options={{ title: "Profile" }} />
<Stack.Screen name="settings" options={{ title: "Settings" }} />
<Stack.Screen name="+not-found" />
</Stack>
<StatusBar style="auto" />
</>
);
}
Tab Layout
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
export default function TabsLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: "#3b82f6",
headerShown: false,
}}
>
<Tabs.Screen
name="index"
options={{
title: "Feed",
tabBarIcon: ({ color }) => <Ionicons name="home" size={24} color={color} />,
}}
/>
<Tabs.Screen
name="search"
options={{
title: "Search",
tabBarIcon: ({ color }) => <Ionicons name="search" size={24} color={color} />,
}}
/>
</Tabs>
);
}
Link and Navigation
// app/(tabs)/index.tsx
import { Link, router } from "expo-router";
import { View, Text, Pressable } from "react-native";
export default function FeedScreen() {
return (
<View>
{/* Declarative Link */}
<Link href="/users/123">View User 123</Link>
{/* With params */}
<Link href={{ pathname: "/users/[id]", params: { id: "456" } }}>
View User 456
</Link>
{/* Programmatic navigation */}
<Pressable onPress={() => router.push("/settings")}>
<Text>Go to Settings</Text>
</Pressable>
{/* Navigate with params */}
<Pressable onPress={() => router.push({ pathname: "/users/[id]", params: { id: "789" } })}>
<Text>View User 789</Text>
</Pressable>
</View>
);
}
API Routes (Server-Side)
// app/api/users/[id]+api.ts — server-side API route
import { ExpoRequest, ExpoResponse } from "expo-router/server";
export async function GET(request: ExpoRequest, { id }: { id: string }) {
const user = await db.users.findUnique({ where: { id } });
if (!user) {
return ExpoResponse.json({ error: "Not found" }, { status: 404 });
}
return ExpoResponse.json(user);
}
Solito: Universal Navigation for Expo + Next.js
Solito is a minimal library that makes React Navigation (for Expo/native) and Next.js Router work with the same API. No separate navigation code for web vs native.
Installation
# In a monorepo (apps/expo + apps/next)
npm install solito
Shared Navigation Components
// packages/app/navigation/link.tsx — shared across Expo and Next.js
import { TextLink } from "solito/link";
import { View } from "react-native";
// Works on both platforms — native uses React Navigation, web uses Next.js Link
export function UserLink({ userId, name }: { userId: string; name: string }) {
return (
<TextLink
href={`/users/${userId}`}
style={{ color: "#3b82f6", textDecorationLine: "underline" }}
>
{name}
</TextLink>
);
}
Shared Router Hook
// packages/app/screens/home.tsx — shared screen component
import { useRouter } from "solito/navigation";
import { useLink } from "solito/link";
export function HomeScreen() {
const router = useRouter();
// Native: React Navigation push
// Web: Next.js router.push
const handlePress = () => router.push("/settings");
return (
<View>
<Text>Home</Text>
<Button title="Settings" onPress={handlePress} />
</View>
);
}
Monorepo Structure
apps/
expo/
app/(tabs)/index.tsx ← wraps packages/app/screens/home.tsx
next/
app/page.tsx ← wraps packages/app/screens/home.tsx
packages/
app/
screens/
home.tsx ← shared, uses solito/navigation
profile.tsx ← shared
navigation/
link.tsx ← shared Link component
Feature Comparison
| Feature | React Navigation v7 | Expo Router | Solito |
|---|---|---|---|
| Routing approach | Config-based | File-based | Adapter layer |
| File-system routes | ❌ | ✅ | Delegates to RN/Next.js |
| Web support | Basic | ✅ First-class | ✅ Via Next.js |
| Deep linking | Manual config | ✅ Automatic | Via Expo Router/Next.js |
| API Routes | ❌ | ✅ (SDK 51+) | ❌ |
| TypeScript params | ✅ v7 static config | ✅ | Partial |
| Tab navigation | ✅ | ✅ | Delegates |
| Drawer navigation | ✅ | ✅ | Delegates |
| Server components | ❌ | ✅ (web) | ✅ (via Next.js) |
| Requires Expo | ❌ | ✅ | ✅ |
| GitHub stars | 25k | ~6k (expo/expo) | ~2.5k |
| Learning curve | Medium | Low (file-based) | Medium |
| Ecosystem maturity | High | Medium | Low |
When to Use Each
Choose Expo Router if:
- Your app uses Expo SDK and you want the modern, opinionated approach
- URL-based deep linking should be automatic (no manual
linkingconfig) - You want server-side API routes in the same project as your mobile app
- File-system routing from Next.js feels familiar and you want it on native
Choose React Navigation if:
- You're using bare React Native (no Expo) or need full control
- You need specific navigator types not yet in Expo Router (material top tabs, side drawer)
- Your team has existing React Navigation v6 code to migrate
- Maximum ecosystem compatibility and third-party library support matters
Choose Solito if:
- You're building a monorepo with shared code between Expo and Next.js
- Code sharing across web and native is the primary architectural goal
- You want the same components, hooks, and navigation logic on both platforms
- You're using Next.js App Router for web and Expo for native
Methodology
Data sourced from GitHub repositories (star counts as of February 2026), official Expo documentation, React Navigation documentation, npm weekly download statistics (January 2026), and community discussions on the React Native Radio podcast and Expo Discord. Feature availability verified against SDK 52 and React Navigation v7 release notes.
Related: React Native vs Expo vs Capacitor for choosing your mobile framework, or NativeWind vs Tamagui vs twrnc for React Native styling.