skills/react-native-skills/rules/navigation-native-navigators.md
Always use native navigators instead of JS-based ones. Native navigators use platform APIs (UINavigationController on iOS, Fragment on Android) for better performance and native behavior.
For stacks: Use @react-navigation/native-stack or expo-router's default
stack (which uses native-stack). Avoid @react-navigation/stack.
For tabs: Use react-native-bottom-tabs (native) or expo-router's native
tabs. Avoid @react-navigation/bottom-tabs when native feel matters.
Incorrect (JS stack navigator):
import { createStackNavigator } from '@react-navigation/stack'
const Stack = createStackNavigator()
function App() {
return (
<Stack.Navigator>
<Stack.Screen name='Home' component={HomeScreen} />
<Stack.Screen name='Details' component={DetailsScreen} />
</Stack.Navigator>
)
}
Correct (native stack with react-navigation):
import { createNativeStackNavigator } from '@react-navigation/native-stack'
const Stack = createNativeStackNavigator()
function App() {
return (
<Stack.Navigator>
<Stack.Screen name='Home' component={HomeScreen} />
<Stack.Screen name='Details' component={DetailsScreen} />
</Stack.Navigator>
)
}
Correct (expo-router uses native stack by default):
// app/_layout.tsx
import { Stack } from 'expo-router'
export default function Layout() {
return <Stack />
}
Incorrect (JS bottom tabs):
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
const Tab = createBottomTabNavigator()
function App() {
return (
<Tab.Navigator>
<Tab.Screen name='Home' component={HomeScreen} />
<Tab.Screen name='Settings' component={SettingsScreen} />
</Tab.Navigator>
)
}
Correct (native bottom tabs with react-navigation):
import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'
const Tab = createNativeBottomTabNavigator()
function App() {
return (
<Tab.Navigator>
<Tab.Screen
name='Home'
component={HomeScreen}
options={{
tabBarIcon: () => ({ sfSymbol: 'house' }),
}}
/>
<Tab.Screen
name='Settings'
component={SettingsScreen}
options={{
tabBarIcon: () => ({ sfSymbol: 'gear' }),
}}
/>
</Tab.Navigator>
)
}
Correct (expo-router native tabs):
// app/(tabs)/_layout.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs'
export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name='index'>
<NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf='house.fill' md='home' />
</NativeTabs.Trigger>
<NativeTabs.Trigger name='settings'>
<NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf='gear' md='settings' />
</NativeTabs.Trigger>
</NativeTabs>
)
}
On iOS, native tabs automatically enable contentInsetAdjustmentBehavior on the
first ScrollView at the root of each tab screen, so content scrolls correctly
behind the translucent tab bar. If you need to disable this, use
disableAutomaticContentInsets on the trigger.
Incorrect (custom header component):
<Stack.Screen
name='Profile'
component={ProfileScreen}
options={{
header: () => <CustomHeader title='Profile' />,
}}
/>
Correct (native header options):
<Stack.Screen
name='Profile'
component={ProfileScreen}
options={{
title: 'Profile',
headerLargeTitleEnabled: true,
headerSearchBarOptions: {
placeholder: 'Search',
},
}}
/>
Native headers support iOS large titles, search bars, blur effects, and proper safe area handling automatically.
Reference: