Wiring a React Navigation stack means maintaining a param-list type, a navigator tree, and a screen registry by hand, all of which have to stay in sync as screens get added or renamed.
#The old way
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomeScreen from './screens/HomeScreen';
import ProfileScreen from './screens/ProfileScreen';
export type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
};
const Stack = createNativeStackNavigator<RootStackParamList>();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
#The new way
The file app/_layout.tsx defines the root stack, and each screen's route is just its path under app/:
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Home' }} />
<Stack.Screen name="profile/[userId]" options={{ title: 'Profile' }} />
</Stack>
);
}
app/profile/[userId].tsx becomes the profile route automatically, with the dynamic segment read via useLocalSearchParams:
import { View, Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';
export default function ProfileScreen() {
const { userId } = useLocalSearchParams<{ userId: string }>();
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Profile for {userId}</Text>
</View>
);
}
import { Link } from 'expo-router';
export function ProfileLink({ userId }: { userId: string }) {
return <Link href={`/profile/${userId}`}>View profile</Link>;
}
#Why it matters
- The file tree is the single source of truth for the nav graph — no separate param-list type to keep in sync by hand.
- Deep links work automatically, since every screen file is already a URL-shaped route.
- Layouts nest naturally through
_layout.tsxfiles instead of hand-nested navigator boilerplate.
#Gotcha
A file under app/ only becomes a route if it has a default export. A stray non-default export, a shared component you accidentally dropped inside app/, or a typo'd filename either fails to register as a route or, worse, silently registers as an unintended one — keep non-route files outside app/ (in components/, for example).