Upgrading from 7.x
React Navigation 8 is still in pre-release stage. The API may still change before the stable release. The upgrade guide will be updated accordingly when we release the stable version.
This guides lists all the breaking changes and new features in React Navigation 8 that you need to be aware of when upgrading from React Navigation 7.
Dependency changes
The minimum required version of React Native, Expo, and TypeScript have been bumped:
react-native>= 0.81 (planned to be bumped to 0.83)expo>= 54 (planned to be bumped to 55)typescript>= 5.9.2 (if you use TypeScript)
The minimum required version of various peer dependencies have also been bumped:
react-native-screens>= 4.19.0react-native-safe-area-context>= 5.5.0react-native-reanimated>= 4.0.0react-native-pager-view>= 7.0.0 (8.0.0 is recommended)react-native-web>= 0.21.0
Previously, many navigators worked without react-native-screens, but now it's required for all navigators.
Additionally, React Navigation now uses @callstack/liquid-glass to implement liquid glass effect on iOS 26.
Expo Go does not include all native dependencies required by React Navigation. So it will not reflect the actual behavior of your app in production. To properly test your app, you need to create a development build of your app.
Breaking changes
Dropping support for old architecture
React Navigation 8 no longer supports the old architecture of React Native. The old architecture has been frozen since React Native 0.80 and removed in React Native 0.82.
So if you're still on the old architecture, you'll need to upgrade to the new architecture in order to use React Navigation 8.
Changes to TypeScript setup
We introduced a static API in React Navigation 7. However, some of the TypeScript types were not inferred and required manual annotations. In React Navigation 8, we reworked the TypeScript types to solve many of these issues.
The root type now uses navigator type instead of param list
Previously the types for the root navigator were specified using declare global and RootParamList. Now, they can be specified with module augmentation of @react-navigation/core and use the navigator's type instead a param list:
type RootStackParamList = StaticParamList<typeof RootStack>;
-
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
type RootStackType = typeof RootStack;
+
declare module '@react-navigation/core' {
interface RootNavigator extends RootStackType {}
}
Using module augmentation is shorter, and avoids namespace usage - which ESLint may complain about in some configurations.
Using the navigator's type instead of a param list allows us to infer the type of navigators - primarily in case of static configuration.
Common hooks no longer accept generics
Previously hooks such as useNavigation, useRoute and useNavigationState accepted a generic to override the default types. This is not type-safe as we cannot verify that the provided type matches the actual navigators, and we recommended minimizing such usage.
In React Navigation 8, we reworked the types to automatically determine the correct type based on the name of the screen when using static config:
const navigation = useNavigation<StackNavigationProp<RootStackParamList, 'Profile'>>();
const navigation = useNavigation('Profile');
If you're using dynamic configuration, unfortunately we cannot currently infer the types automatically. So it still requires manual annotation. However, now you need to use as instead of generics to make it clearer that this is unsafe:
const navigation = useNavigation<StackNavigationProp<RootStackParamList, 'Profile'>>();
const navigation = useNavigation() as StackNavigationProp<RootStackParamList, 'Profile'>;
The useRoute type has been updated in the same way:
const route = useRoute<RouteProp<RootStackParamList, 'Profile'>>();
const route = useRoute('Profile');
And if you're using dynamic configuration:
const route = useRoute<RouteProp<RootStackParamList, 'Profile'>>();
const route = useRoute() as RouteProp<RootStackParamList, 'Profile'>;
Similarly, the useNavigationState type has been updated to accept the name of the screen in addition to the selector:
const focusedRouteName = useNavigationState<RootStackParamList>((state) => state.routes[state.index].name);
const focusedRouteName = useNavigationState('Settings', (state) => state.routes[state.index].name);
If you're using dynamic configuration, you can use as:
const focusedRouteName = useNavigationState<RootStackParamList>((state) => state.routes[state.index].name);
const focusedRouteName = useNavigationState((state) => state.routes[state.index].name as keyof RootStackParamList);
New createXScreen API for creating screen config
One of the limitations of the static config API is that the type of route object can't be inferred in screen callback, listeners callback etc. This made it difficult to use route params in these callbacks.
To address this, we added a new createXScreen API for each navigator to create screen config with proper types:
const Stack = createStackNavigator({
screens: {
Profile: {
screen: ProfileScreen,
options: ({ route }) => {
const userId = route.params.userId; // Don't know the type of route params
-
return { title: `User ${userId}` };
},
},
Profile: createStackScreen({
screen: ProfileScreen,
options: ({ route }) => {
const userId = route.params.userId; // Now correctly inferred
+
return { title: `User ${userId}` };
},
});
}
});
When using the createXScreen API, the type of params are automatically inferred based on the type annotation for the component specified in screen (e.g. (props: StaticScreenProps<ProfileParams>)) and the path pattern specified in the linking config (e.g. linking: 'profile/:userId').
Each navigator exports its own helper function, e.g. createNativeStackScreen for Native Stack Navigator, createBottomTabScreen for Bottom Tab Navigator, createDrawerScreen for Drawer Navigator etc.
See Static configuration docs for more details.
Custom navigators now require overloads for types
To work with the reworked TypeScript types, custom navigators now need to provide overloads for static and dynamic configuration APIs, and an additional API to create screen config.
export function createMyNavigator<
const ParamList extends ParamListBase,
const NavigatorID extends string | undefined = string | undefined,
const TypeBag extends NavigatorTypeBagBase = {
ParamList: ParamList;
NavigatorID: NavigatorID;
State: TabNavigationState<ParamList>;
ScreenOptions: MyNavigationOptions;
EventMap: MyNavigationEventMap;
NavigationList: {
[RouteName in keyof ParamList]: MyNavigationProp<
ParamList,
RouteName,
NavigatorID
>;
};
Navigator: typeof MyNavigator;
},
const Config extends StaticConfig<TypeBag> = StaticConfig<TypeBag>,
>(config?: Config): TypedNavigator<TypeBag, Config> {
return createNavigatorFactory(MyNavigator)(config);
}
type MyTypeBag<ParamList extends {}> = {
ParamList: ParamList;
State: TabNavigationState<ParamList>;
ScreenOptions: MyNavigationOptions;
EventMap: MyNavigationEventMap;
NavigationList: {
[RouteName in keyof ParamList]: MyNavigationProp<
ParamList,
RouteName
>;
};
Navigator: typeof MyNavigator;
};
+
export function createMyNavigator<
const ParamList extends ParamListBase,
>(): TypedNavigator<MyTypeBag<ParamList>, undefined>;
export function createMyNavigator<
const Config extends StaticConfig<MyTypeBag<ParamListBase>>,
>(
config: Config
): TypedNavigator<
MyTypeBag<StaticParamList<{ config: Config }>>,
Config
>;
export function createMyNavigator(config?: unknown) {
return createNavigatorFactory(MyNavigator)(config);
}
export function createMyScreen<
const Linking extends StaticScreenConfigLinking,
const Screen extends StaticScreenConfigScreen,
>(
config: StaticScreenConfig<
Linking,
Screen,
TabNavigationState<ParamListBase>,
MyNavigationOptions,
MyNavigationEventMap,
MyNavigationProp<ParamListBase>
>
) {
return config;
}
See Custom navigators for more details.
Changes to navigators
Native Bottom Tabs are now default
Previously, the Bottom Tab Navigator used a JavaScript-based implementation and a native implementation was available under @react-navigation/bottom-tabs/unstable. Native bottom tabs are not used by default on iOS and Android. This allows us to match the new native design such as liquid glass effect on iOS 26.
The @react-navigation/bottom-tabs/unstable entry point has been removed.
To keep the previous behavior with JavaScript-based tabs, you can pass implementation: 'custom' to the navigator:
- Static
- Dynamic
createBottomTabNavigator({
implementation: 'custom',
// ...
});
<Tab.Navigator
implementation="custom"
// ...
>
As part of this change, some of the options have changed to work with native tabs:
tabBarShowLabelis replaced withtabBarLabelVisibilityModewhich accepts:"auto"(default)"selected""labeled"- same astabBarShowLabel: true"unlabeled"- same astabBarShowLabel: false
tabBarLabelnow only accepts astringtabBarIconnow accepts an function that returns an icon object
The following props have been removed:
safeAreaInsetsfrom the navigator propsinsetsfrom the bottom tab bar propslayoutfrom the bottom tab bar props
See the Bottom Tab Navigator docs for all the available options.
Bottom Tabs no longer shows header by default
Since Bottom Tabs now renders native tabs by default, the header is no longer shown by default to match native look and feel. You can nest a Native Stack Navigator inside each tab to show a header that integrates well with native tabs, e.g. search tab on iOS 26+.
Alternatively, you can enable the built-in header by passing headerShown: true in screenOptions of the navigator:
- Static
- Dynamic
createBottomTabNavigator({
screenOptions: {
headerShown: true,
// ...
},
// ...
});
<Tab.Navigator
screenOptions={{
headerShown: true,
// ...
}}
>
Navigators no longer accept an id prop
Previously, navigators accepted an id prop to identify them - which was used with navigation.getParent(id) to get a parent navigator by id. However, there were a couple of issues with this approach:
- It wasn't well integrated with TypeScript types, and required manual annotations.
- The navigation object is specific to a screen, so using the navigator's id was inconsistent.
- It was used for a very specific use case, so it added unnecessary complexity.
In React Navigation 8, we removed the id prop from navigators. Instead, you can use the screen's name to get a parent navigator:
const parent = navigation.getParent('some-id');
const parent = navigation.getParent('SomeScreenName');
In this case, 'SomeScreenName' refers to the name of a parent screen that's used in the navigator.
See navigation object docs for more details.
setParams no longer pushes to history in tab and drawer navigators when backBehavior is set to fullHistory
Previously, when using setParams in tab and drawer navigators with backBehavior set to fullHistory, it would push a new entry to the history stack.
In React Navigation 8, we added a new pushParams action that achieves this behavior. So setParams now only updates the params without affecting the history stack.
navigation.setParams({ filter: 'new' });
navigation.pushParams({ filter: 'new' });
This way you have more control over how params are updated in tab and drawer navigators.
See setParams action docs for more details.
Navigators no longer use InteractionManager
Previously, various navigators used InteractionManager to mark when animations and gestures were in progress. This was primarily used to defer code that should run after transitions, such as loading data or rendering heavy components.
However, InteractionManager has been deprecated in latest React Native versions, so we are removing support for this API in React Navigation 8. As an alternative, consumers can listen to events such as transitionStart, transitionEnd etc. when applicable:
InteractionManager.runAfterInteractions(() => {
// code to run after transition
});
navigation.addListener('transitionEnd', () => {
// code to run after transition
});
Keep in mind that unlike InteractionManager which is global, the transition events are specific to a navigator.
If you have a use case that cannot be solved with transition events, please open a discussion on GitHub.
The color arguments in various navigators now accept ColorValue
Previously, color options in various navigators only accepted string values. In React Navigation 8, these options now accept ColorValue to match the changes to theming.
Unless you are using a custom theme with PlatformColor or DynamicColorIOS etc, this change only breaks TypeScript types:
const tabBarIcon = ({ color, size }: { color: string, size: number }) => {
const tabBarIcon = ({ color, size }: { color: ColorValue, size: number }) => {
// ...
};
See Themes for more information about dynamic colors.
Various components no longer receive layout related props
Previously, various components such as Header, BottomTabBar, and DrawerContent received layout related props such as layout - that contained the dimensions of the screen.
This meant that if the layout changed frequently, such as resizing the window on supported platforms (Web, Windows, macOS, iPadOS), it would need to re-render these components frequently - often not being able to keep up with the changes, leading to jank and poor performance.
To avoid this, we have removed layout related props from these components:
layoutprop fromHeadercomponent from@react-navigation/elementstitleLayoutandscreenLayoutprops fromHeaderBackButtoncomponent from@react-navigation/elementslayouts.titleandlayouts.leftLabelparameters fromheaderStyleInterpolatorin@react-navigation/stacklayoutprop fromreact-native-tab-viewlayoutprop fromreact-native-drawer-layout
Since React Native doesn't provide APIs to handle layout changes in styles, it may still be necessary to handle layout changes manually in some cases. So we have added a useFrameSize hook that takes a selector function to minimize re-renders:
import { useFrameSize } from '@react-navigation/elements';
// ...
const isLandscape = useFrameSize((size) => size.width > size.height);
The onChangeText callback has been renamed to onChange for headerSearchBarOptions
The onChangeText option in headerSearchBarOptions was confusingly named after text input's
onChangeText, but TextInput's onChangeText receives the new text as the first argument, whereas headerSearchBarOptions.onChangeText received an event object - similar to TextInput's onChange.
To avoid confusion due to this inconsistency, the option has been renamed to onChange. To upgrade, simply rename the option:
- Static
- Dynamic
createNativeStackNavigator({
screens: {
Search: {
screen: SearchScreen,
options: {
headerSearchBarOptions: {
onChangeText: (event) => {
onChange: (event) => {
const text = event.nativeEvent.text;
// ...
},
},
},
},
},
});
<Stack.Navigator>
<Stack.Screen
name="Search"
component={SearchScreen}
options={{
headerSearchBarOptions: {
onChangeText: (event) => {
onChange: (event) => {
const text = event.nativeEvent.text;
// ...
},
},
}}
/>
</Stack.Navigator>
This applies to all navigators that support headerSearchBarOptions, such as Native Stack Navigator with native header, and other navigators using Header from @react-navigation/elements.
If you're using Header from @react-navigation/elements directly, the same change applies.
APIs for customizing Navigation bar and status bar colors are removed from Native Stack Navigator
Previously, Native Stack Navigator provided options to customize the appearance of the navigation bar and status bar on Android:
navigationBarColornavigationBarTranslucentstatusBarBackgroundColorstatusBarTranslucent
In Android 15 and onwards, edge-to-edge is now the default behavior, and will likely be enforced in future versions. Therefore, these options have been removed in React Navigation 8.
You can use react-native-edge-to-edge instead to configure status bar and navigation bar related settings.
See Native Stack Navigator for all available options.
Stack Navigator now accepts a number for gestureResponseDistance
Previously, the gestureResponseDistance option in Stack Navigator accepted an object with horizontal and vertical properties to specify the distance for gestures. Since it's not pssible to have both horizontal and vertical gestures at the same time, it now accepts a number to specify the distance for the current gesture direction:
gestureResponseDistance: { horizontal: 50 }
gestureResponseDistance: 50
Drawer Navigator now accepts overlayStyle instead of overlayColor
Previously, the Drawer Navigator accepted an overlayColor prop to customize the color of the overlay that appears when the drawer is open. It now accepts overlayStyle prop instead to provide more flexibility for styling the overlay:
overlayColor="rgba(0, 0, 0, 0.5)"
overlayStyle={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }}
See Drawer Navigator for more details.
Miscellaneous
Various deprecated APIs have been removed
The following API that were marked as deprecated in React Navigation 7 have been removed:
-
navigateDeprecatedfrom the navigation object has been removed. Usenavigateinstead. To preserve the previous behavior, you can passpop: trueas the third argument tonavigate:navigation.navigateDeprecated('Profile', { userId: 123 });
navigation.navigate('Profile', { userId: 123 }, { pop: true }); -
getIdfrom the navigation object has been removed since theidprop has been removed. -
navigationInChildEnabledprop fromNavigationContainerhas been removed. This behavior is no longer supported.
The linking config no longer requires a prefixes option
Previously, the linking configuration required a prefixes option to specify the URL prefixes that the app should handle. This historical reason for this is to support Expo Go which uses a custom URL scheme.
Since then, the recommended way to develop with Expo has been to use Development Builds, which use the app's own URL scheme. So the prefixes option is not needed for most use cases.
You can now omit the prefixes option in the linking configuration unless you're using Expo Go:
- Static
- Dynamic
<Navigation
linking={{
prefixes: ['myapp://', 'https://myapp.com'],
enabled: 'auto',
}}
>
<NavigationContainer
linking={{
prefixes: ['myapp://', 'https://myapp.com'],
config: { /* ... */ }
}}
>
The prefixes default to ['*'], which will match any host starting with http, https, and custom schemes such as myapp://.
See Configuring links for more details.
Some exports are removed from @react-navigation/elements
The @react-navigation/elements package has exported some components that were primarily intended for internal usage. These components have been removed from the public API:
-
BackgroundBackground color can instead be applied by using it from
useTheme.import { Background } from '@react-navigation/elements';
import { useTheme } from '@react-navigation/native';
// ...
<Background>{children}</Background>
const { colors } = useTheme();
+
<View style={{ backgroundColor: colors.background }}>{children}</View> -
ScreenYou can render the
Headercomponent directly instead. -
SafeAreaProviderCompatYou can use
SafeAreaProviderfromreact-native-safe-area-contextdirectly instead. -
MissingIconYou can copy the implementation from the source code if you need a placeholder icon.
Some of these components are still available and exported at @react-navigation/elements/internal, so you can continue using them if you really need. However, since they are not part of the public API, they don't follow semver and may change without warning in future releases.
The getDefaultHeaderHeight utility now accepts an object instead of positional arguments
The getDefaultHeaderHeight utility from @react-navigation/elements now accepts an object with named properties instead of positional arguments to improve readability"
getDefaultHeaderHeight(layout, false, statusBarHeight);
getDefaultHeaderHeight({
landscape: false,
modalPresentation: false,
topInset: statusBarHeight
});
See Elements docs for more details.
New features
Common hooks now accept name of the screen
The useNavigation, useRoute, and useNavigationState hooks can now optionally accept the name of the screen:
const route = useRoute('Profile');
The name of the screen can be for the current screen or any of its parent screens. This makes it possible to get params and navigation state for a parent screen without needing to setup context to pass them down.
If the provided screen name does not exist in any of the parent screens, it will throw an error, so any mistakes are caught early.
When using static configuration, the types are automatically inferred based on the name of the screen.
It's still possible to use these hooks without passing the screen name, same as before, and it will return the navigation or route for the current screen.
See useNavigation, useRoute, and useNavigationState for more details.
New entry can be added to history stack with pushParams action
The pushParams action updates the params and pushes a new entry to the history stack:
navigation.pushParams({ filter: 'new' });
Unlike setParams, this does not merge the new params with the existing ones. Instead, it uses the new params object as-is.
The action works in all navigators, such as stack, tab, and drawer. This allows to add a new entry to the history stack without needing to push a new screen instance.
This can be useful in various scenario:
- A product listing page with filters, where changing filters should create a new history entry so that users can go back to previous filter states.
- A screen with a custom modal component, where the modal is not a separate screen in the navigator, but its state should be reflected in the URL and history.
See pushParams docs for more details.
Themes now support ColorValue and CSS custom properties
Previously, theme colors only supported string values. In React Navigation 8, theme colors now support PlatformColor, DynamicColorIOS on native, and CSS custom properties on Web for more flexibility.
Example theme using PlatformColor:
const MyTheme = {
...DefaultTheme,
colors: Platform.select({
ios: () => ({
primary: PlatformColor('systemRed'),
background: PlatformColor('systemGroupedBackground'),
card: PlatformColor('tertiarySystemBackground'),
text: PlatformColor('label'),
border: PlatformColor('separator'),
notification: PlatformColor('systemRed'),
}),
android: () => ({
primary: PlatformColor('@android:color/system_primary_light'),
background: PlatformColor(
'@android:color/system_surface_container_light'
),
card: PlatformColor('@android:color/system_background_light'),
text: PlatformColor('@android:color/system_on_surface_light'),
border: PlatformColor('@android:color/system_outline_variant_light'),
notification: PlatformColor('@android:color/holo_red_light'),
}),
default: () => DefaultTheme.colors,
})(),
};
See Themes for more details.
Groups now support linking option in static configuration
The linking option can now be specified for groups in static configuration to define nested paths:
const Stack = createStackNavigator({
groups: {
Settings: {
linking: { path: 'settings' },
screens: {
UserSettings: 'user',
AppSettings: 'app',
},
},
},
});
This lets you prefix the paths of the screens in the group with a common prefix, e.g. settings/ for settings/user and settings/app.
See Group for more details.
Deep linking to screens behind conditional screens is now supported
Previously, if a screen was conditionally rendered based on some state (e.g. authentication status), deep linking to that screen wouldn't work since the screen wouldn't exist in the navigator when the app was opened via a deep link.
In React Navigation 7, we added an experimental UNSTABLE_routeNamesChangeBehavior option to enable remembering such unhandled actions and re-attempting them when the list of route names changed after the conditions changed by setting the option to lastUnhandled.
In React Navigation 8, we have dropped the UNSTABLE_ prefix and made it a stable API.
- Static
- Dynamic
const Stack = createNativeStackNavigator({
routeNamesChangeBehavior: 'lastUnhandled',
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});
const Stack = createNativeStackNavigator();
function Stack() {
return (
<Stack.Navigator routeNamesChangeBehavior="lastUnhandled">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
);
}
Navigators now accept a router prop
A router defines how the navigator updates its state based on navigation actions. Previously, custom routers could only be used by creating a custom navigator.
We later added an experimental UNSTABLE_router prop to various navigators to customize the router without needing to create a custom navigator. In React Navigation 8, we have dropped the UNSTABLE_ prefix and made it a stable API.
- Static
- Dynamic
const MyStack = createNativeStackNavigator({
router: (original) => ({
getStateForAction(state, action) {
if (action.type === 'NAVIGATE') {
// Custom logic for NAVIGATE action
}
// Fallback to original behavior
return original.getStateForAction(state, action);
},
}),
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});
const Stack = createNativeStackNavigator();
function MyStack() {
return (
<Stack.Navigator
router={(original) => ({
getStateForAction(state, action) {
if (action.type === 'NAVIGATE') {
// Custom logic for NAVIGATE action
}
// Fallback to original behavior
return original.getStateForAction(state, action);
},
})}
>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
);
}
See Navigator docs for more details.
Header from @react-navigation/elements has been reworked
The Header component from @react-navigation/elements has been reworked with various improvements:
- It uses the new liquid glass effect on iOS 26
- It supports
ColorValueand CSS custom properties for colors - It supports
headerBlurEffecton Web (previously only supported on iOS in Native Stack Navigator) - It no longer needs the layout of the screen to render correctly
To match the iOS 26 design, the back button title is no longer shown by default on iOS 26.
See Elements for more details.
react-native-tab-view now supports a renderAdapter prop for custom adapters
By default, react-native-tab-view uses react-native-pager-view for rendering pages on Android and iOS. However, it may not be suitable for all use cases.
So it now supports a renderAdapter prop to provide a custom adapter for rendering pages. For example, you can use ScrollViewAdapter to use a ScrollView for rendering pages:
import React from 'react';
import { TabView, ScrollViewAdapter } from 'react-native-tab-view';
export default function TabViewExample() {
const [index, setIndex] = React.useState(0);
return (
<TabView
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
renderAdapter={ScrollViewAdapter}
/>
);
}
You can also create your own custom adapter by implementing the required interface. See the react-native-tab-view docs for more information.
State persistence is simplified with the persistor prop
Previously, state persistence could be implemented with initialState and onStateChange props, however it required some boilerplates and handling edge cases.
The new persistor prop simplifies state persistence by reducing the boilerplate code needed to persist and restore state:
- Static
- Dynamic
export default function App() {
return (
<Navigation
persistor={{
async persist(state) {
await AsyncStorage.setItem(
'NAVIGATION_STATE_V1',
JSON.stringify(state)
);
},
async restore() {
const state = await AsyncStorage.getItem('NAVIGATION_STATE_V1');
return state ? JSON.parse(state) : undefined;
},
}}
/>
);
}
export default function App() {
return (
<NavigationContainer
persistor={{
async persist(state) {
await AsyncStorage.setItem(
'NAVIGATION_STATE_V1',
JSON.stringify(state)
);
},
async restore() {
const state = await AsyncStorage.getItem('NAVIGATION_STATE_V1');
return state ? JSON.parse(state) : undefined;
},
}}
>
{/* ... */}
</NavigationContainer>
);
}
See State persistence docs for more details.