Fix: React Navigation Not Working — Screens Not Rendering, TypeScript Errors, or Gestures Broken
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix React Navigation issues — stack and tab navigator setup, TypeScript typing, deep linking, screen options, nested navigators, authentication flow, and performance optimization.
The Problem
Screens don’t render after setting up React Navigation:
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
// White screen or crash — no navigation renderedOr gestures (swipe back) don’t work:
Stack screens render but swipe-to-go-back doesn't functionOr TypeScript complains about navigation props:
Property 'navigate' does not exist on type '{}'Why This Happens
React Navigation is the standard navigation library for React Native. It is a thin coordination layer on top of several native modules, and the failures usually come from a missing peer dependency, the wrong import order, or a stale Metro cache after upgrading.
Peer dependencies must all be installed. React Navigation has hard requirements on react-native-screens (for the native stack to call into UINavigationController on iOS and Fragment on Android), react-native-safe-area-context (for safe-area insets that the navigators consume), and react-native-gesture-handler (for swipe-back, drawer, and bottom-tab gestures). Missing any of them either crashes at module load with “Native module not found” or produces a navigator that renders but does not respond to gestures. On bare React Native projects you also have to run cd ios && pod install after installing — on Expo, npx expo install handles the autolinking and the iOS pods through the prebuild step.
react-native-gesture-handler must be imported first. It monkey-patches React Native’s gesture system at module load. If it loads after any component that uses gestures, the patching happens too late and the gestures silently no-op. The fix is to put import 'react-native-gesture-handler'; at the very top of App.tsx or index.js, above any other import. Expo Router projects do this for you in the generated entry; bare React Native projects do not.
TypeScript needs explicit navigator typing. navigation.navigate('Screen') is typed as accepting any string by default, which means typos compile silently. The right pattern is to declare a RootStackParamList (mapping screen names to their params), pass it as a generic to createNativeStackNavigator<RootStackParamList>(), and augment the global ReactNavigation.RootParamList so useNavigation() is typed everywhere without per-call annotations.
Native stack and JS stack are different packages with different trade-offs. @react-navigation/native-stack uses the platform’s native navigation controllers — fast, animations feel native, but it requires react-native-screens and some screen options are platform-only. @react-navigation/stack is a JS-based implementation that works without react-native-screens but is slower and less native-feeling. Most current projects use the native stack; older guides may still recommend the JS stack.
Version History: React Navigation from v5 to v7
React Navigation 5 was released in March 2020 and was the inflection point for the library. v5 introduced the static <Stack.Navigator> and <Stack.Screen> component-based API that replaced the v4 configuration-based createSwitchNavigator/createStackNavigator calls. It also added the hooks-based API (useNavigation, useRoute, useFocusEffect) which is now the dominant pattern. Most “modern React Navigation” tutorials you find online assume the v5+ shape.
React Navigation 6 shipped in July 2021 and tightened the developer experience. v6 dropped the v4-compatible API surface, made TypeScript typing first-class with NativeStackScreenProps and CompositeScreenProps, introduced the native stack as the recommended default, added the screenOptions callback that receives route and navigation, and reworked the linking config for deep linking. The v5-to-v6 migration was straightforward (mostly import renames and option key changes) and most production apps now run v6.
React Navigation 7 was released in October 2024 and introduced the new static API. The static API lets you declare your navigators as plain objects with createStaticNavigation instead of nested JSX components. The dynamic API (the <Stack.Navigator> / <Stack.Screen> JSX you have been writing since v5) still works and is not deprecated; v7 just adds the static option for teams that want type inference without manually maintaining param lists. v7 also tightened the peer dependency requirements (React 18, React Native 0.72+) and improved the typing for nested navigators so CompositeScreenProps is needed less often.
The closest competitor today is Expo Router, which Expo released in late 2023 as a file-system-based router that wraps React Navigation underneath. With Expo Router, you create files in app/ and the routes are inferred from the file structure — no JSX navigator config at all. Expo Router uses React Navigation’s primitives internally, so the behaviors (screen options, deep linking, headers, tabs) are the same; the difference is purely in how you declare the routes. Choose React Navigation directly when you want full control over the navigator configuration or you are not on Expo. Choose Expo Router when you want the file-system routing model and you are already on Expo SDK 50+. The two are not mutually exclusive — Expo Router is built on React Navigation and you can drop into the underlying APIs when you need to.
Fix 1: Complete Setup
npm install @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs
npx expo install react-native-screens react-native-safe-area-context react-native-gesture-handler// App.tsx — entry point
import 'react-native-gesture-handler'; // Must be first import
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
// Type definitions for navigation
type RootStackParamList = {
MainTabs: undefined;
Details: { id: string; title: string };
Settings: undefined;
Modal: { message: string };
};
type TabParamList = {
Home: undefined;
Search: { query?: string };
Profile: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
const Tab = createBottomTabNavigator<TabParamList>();
function MainTabs() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
const icons: Record<string, string> = {
Home: focused ? 'home' : 'home-outline',
Search: focused ? 'search' : 'search-outline',
Profile: focused ? 'person' : 'person-outline',
};
return <Ionicons name={icons[route.name] as any} size={size} color={color} />;
},
tabBarActiveTintColor: '#3b82f6',
tabBarInactiveTintColor: '#9ca3af',
})}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}
export default function App() {
return (
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="MainTabs"
component={MainTabs}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Details"
component={DetailsScreen}
options={({ route }) => ({ title: route.params.title })}
/>
<Stack.Screen name="Settings" component={SettingsScreen} />
<Stack.Screen
name="Modal"
component={ModalScreen}
options={{ presentation: 'modal' }}
/>
</Stack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
);
}Fix 2: Type-Safe Navigation
// types/navigation.ts
import type { NativeStackScreenProps, NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import type { CompositeScreenProps, NavigatorScreenParams } from '@react-navigation/native';
// Define param lists for each navigator
export type RootStackParamList = {
MainTabs: NavigatorScreenParams<TabParamList>;
Details: { id: string; title: string };
Settings: undefined;
};
export type TabParamList = {
Home: undefined;
Search: { query?: string };
Profile: undefined;
};
// Screen prop types
export type RootStackScreenProps<T extends keyof RootStackParamList> =
NativeStackScreenProps<RootStackParamList, T>;
export type TabScreenProps<T extends keyof TabParamList> =
CompositeScreenProps<
BottomTabScreenProps<TabParamList, T>,
NativeStackScreenProps<RootStackParamList>
>;
// For useNavigation hook
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}// screens/HomeScreen.tsx — typed screen component
import type { TabScreenProps } from '@/types/navigation';
import { View, Text, Button } from 'react-native';
export default function HomeScreen({ navigation }: TabScreenProps<'Home'>) {
return (
<View style={{ flex: 1, padding: 16 }}>
<Text>Home Screen</Text>
<Button
title="Open Details"
onPress={() => navigation.navigate('Details', { id: '123', title: 'My Item' })}
// TypeScript ensures id and title are provided
/>
<Button
title="Open Modal"
onPress={() => navigation.navigate('Modal', { message: 'Hello!' })}
/>
</View>
);
}
// Using useNavigation hook (any component, not just screens)
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RootStackParamList } from '@/types/navigation';
function SomeButton() {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
return (
<Button
title="Go to Settings"
onPress={() => navigation.navigate('Settings')}
/>
);
}Fix 3: Screen Options and Headers
<Stack.Navigator
screenOptions={{
headerStyle: { backgroundColor: '#ffffff' },
headerTintColor: '#1a1a2e',
headerTitleStyle: { fontWeight: '600' },
headerShadowVisible: false,
animation: 'slide_from_right', // 'default' | 'fade' | 'slide_from_right' | 'slide_from_bottom' | 'none'
}}
>
<Stack.Screen
name="Details"
component={DetailsScreen}
options={({ route, navigation }) => ({
title: route.params.title,
headerRight: () => (
<Pressable onPress={() => navigation.navigate('Settings')}>
<Ionicons name="settings-outline" size={24} color="#333" />
</Pressable>
),
// Custom header
// header: (props) => <CustomHeader {...props} />,
// Hide back button text (iOS)
headerBackTitle: '',
// Full screen modal
// presentation: 'fullScreenModal',
})}
/>
</Stack.Navigator>
// Set options dynamically from within a screen
function DetailsScreen({ navigation, route }) {
useLayoutEffect(() => {
navigation.setOptions({
title: `Item ${route.params.id}`,
headerRight: () => <ShareButton />,
});
}, [navigation, route.params.id]);
return <View>...</View>;
}Fix 4: Deep Linking
const linking = {
prefixes: ['myapp://', 'https://myapp.com'],
config: {
screens: {
MainTabs: {
screens: {
Home: '',
Search: 'search',
Profile: 'profile',
},
},
Details: 'details/:id',
Settings: 'settings',
},
},
};
<NavigationContainer linking={linking} fallback={<LoadingScreen />}>
<Stack.Navigator>
{/* ... */}
</Stack.Navigator>
</NavigationContainer>
// Links:
// myapp:// → Home tab
// myapp://search → Search tab
// myapp://details/123 → Details screen with id="123"
// https://myapp.com/settings → Settings screenFix 5: Authentication Flow
export default function App() {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) return <SplashScreen />;
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
// Authenticated screens
<>
<Stack.Screen name="MainTabs" component={MainTabs} />
<Stack.Screen name="Details" component={DetailsScreen} options={{ headerShown: true }} />
<Stack.Screen name="Settings" component={SettingsScreen} options={{ headerShown: true }} />
</>
) : (
// Auth screens
<>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
</>
)}
</Stack.Navigator>
</NavigationContainer>
);
}
// When isAuthenticated changes, React Navigation automatically
// replaces the navigation state — no manual navigation neededFix 6: Performance Optimization
// Lazy load screens
const SettingsScreen = React.lazy(() => import('./screens/SettingsScreen'));
// Or use the lazy prop
<Stack.Screen name="Settings" getComponent={() => require('./screens/SettingsScreen').default} />
// Freeze inactive screens (reduce re-renders)
import { enableFreeze } from 'react-native-screens';
enableFreeze(true); // Call at app startup
// Avoid anonymous functions in screen options
// WRONG — creates new function every render
<Stack.Screen options={() => ({ title: 'Home' })} />
// CORRECT — stable reference
const homeOptions = { title: 'Home' };
<Stack.Screen options={homeOptions} />Still Not Working?
White screen or crash on startup — a peer dependency is missing. Install all required packages: react-native-screens, react-native-safe-area-context, react-native-gesture-handler. For Expo, use npx expo install to get compatible versions.
Gestures (swipe back) don’t work — import 'react-native-gesture-handler' must be the first import in your entry file. If using bare React Native (not Expo), run cd ios && pod install after installing.
TypeScript errors on navigation — define RootParamList in the global ReactNavigation namespace. This enables useNavigation() to be typed across all components without explicit type annotations.
Nested navigator shows double headers — set headerShown: false on the parent screen that contains the nested navigator. Each navigator renders its own header by default.
Tutorial code from before v6 does not compile — the v5-to-v6 migration renamed several import paths and option keys. react-native-screens/native-stack moved to @react-navigation/native-stack. useNavigation and useRoute are now in @react-navigation/native. If you are following a v5 tutorial on a v6 project, the screen options keys (headerStyle, headerTintColor) are mostly the same but the linking config shape changed. The v7 release (October 2024) did not break the v6 component API — your existing <Stack.Navigator> code keeps working — but it adds the optional static API via createStaticNavigation.
navigation.navigate('Screen') works in dev but throws “Screen not handled” in production — Hermes (the JS engine used by React Native production builds) is stricter about non-existent routes than the dev bundle. Confirm the screen name is registered in a navigator that is currently mounted. With nested navigators, you may need to pass the parent navigator’s screen plus the child: navigation.navigate('MainTabs', { screen: 'Profile' }).
Deep links open the app but always land on the home screen — the linking config is missing or does not match the URL pattern. Verify prefixes contains your scheme (myapp://) and the universal link domain (https://myapp.com), and that every screen you want to be deep-linkable appears under config.screens. On iOS, also confirm the Associated Domains entitlement is set; on Android, confirm the intent filter in AndroidManifest.xml is set with autoVerify="true".
For related mobile issues, see Fix: Expo Router Not Working, Fix: Expo Not Working, Fix: React Native Reanimated Not Working, and Fix: React Native Metro Bundler Failed.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Expo Router Not Working — Routes Not Matching, Layout Nesting Wrong, or Deep Links Failing
How to fix Expo Router issues — file-based routing, layout routes, dynamic segments, tabs and stack navigation, modal routes, authentication flows, and deep linking configuration.
Fix: React Native Paper Not Working — Theme Not Applying, Icons Missing, or Components Unstyled
How to fix React Native Paper issues — PaperProvider setup, Material Design 3 theming, custom color schemes, icon configuration, dark mode, and Expo integration.
Fix: NativeWind Not Working — Styles Not Applying, Dark Mode Broken, or Metro Bundler Errors
How to fix NativeWind issues — Tailwind CSS for React Native setup, Metro bundler configuration, className prop, dark mode, responsive styles, and Expo integration.
Fix: Tamagui Not Working — Styles Not Applying, Compiler Errors, or Web/Native Mismatch
How to fix Tamagui UI kit issues — setup with Expo, theme tokens, styled components, animations, responsive props, media queries, and cross-platform rendering.