Static API vs Dynamic API
React Navigation provides two ways to configure your navigation:
- Static API - object-based configuration with automatic TypeScript types and deep linking
- Dynamic API - component-based dynamic configuration
If you're already familiar with the dynamic API, this guide explains how each concept maps to the static API - whether you're migrating an existing app or just learning the static API.
Limitations
Since the static API is designed for static configuration, there are some limitations to be aware of:
- The navigation tree must be static - you cannot create the list of screens dynamically. However, you can conditionally render screens using the
ifproperty. - React hooks cannot be used in
options,listenersetc. However,React.use()can be used to read context values inoptionscallback (though it may produce ESLint warnings since ESLint cannot detect that it runs during render), and you can use thethemeparameter instead ofuseTheme().
Basic usage
In the dynamic API, navigators are React components rendered inside NavigationContainer. In the static API, you pass the configuration object to createXNavigator and render the component returned by createStaticNavigation:
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
function RootStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
);
}
function App() {
return (
<NavigationContainer>
<RootStack />
</NavigationContainer>
);
}
import * as React from 'react';
import { createStaticNavigation } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const RootStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
},
});
const Navigation = createStaticNavigation(RootStack);
function App() {
return <Navigation />;
}
The component returned by createStaticNavigation accepts the same props as NavigationContainer.
Navigation object
Screens no longer receive the navigation object as a prop in the static API. It's necessary to use the useNavigation hook instead:
function HomeScreen({ navigation }) {
return (
<Button
title="Go to profile"
onPress={() => navigation.navigate('Profile')}
/>
);
}
function HomeScreen() {
const navigation = useNavigation('Home');
return (
<Button
title="Go to profile"
onPress={() => navigation.navigate('Profile')}
/>
);
}
The route prop is still passed to the screen component as a prop in the static API. But it maybe preferable to use the useRoute hook when defining types with linking.
Navigator options
In the dynamic API, navigator configuration is passed as props to the navigator component. In the static API, they become top-level keys in the config object.
The screens are defined in a screens property instead of as children. It contains a mapping of screen name to screen components, a nested navigator, or a screen configuration object.
<Stack.Navigator
initialRouteName="Home"
screenOptions={{
headerTintColor: 'white',
headerStyle: { backgroundColor: 'tomato' },
}}
>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
const RootStack = createNativeStackNavigator({
initialRouteName: 'Home',
screenOptions: {
headerTintColor: 'white',
headerStyle: { backgroundColor: 'tomato' },
},
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
});
Screen configuration
All props passed to <Stack.Screen> except name and component become properties in the screen configuration object. The component passed to component becomes the value of the screen property in the screen config:
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={({ route }) => ({
title: route.params.userId,
})}
listeners={{
focus: () => console.log('focused'),
}}
getId={({ params }) => params.userId}
/>
import { createNativeStackScreen } from '@react-navigation/native-stack';
const RootStack = createNativeStackNavigator({
screens: {
Profile: createNativeStackScreen({
screen: ProfileScreen,
options: ({ route }) => ({
title: route.params.userId,
}),
listeners: {
focus: () => console.log('focused'),
},
getId: ({ params }) => params.userId,
}),
},
});
The createXScreen helper is for type inference in options and listeners callbacks. Each navigator exports its own screen helper: createNativeStackScreen, createStackScreen, createBottomTabScreen, createDrawerScreen, createMaterialTopTabScreen.
Conditional screens
In the dynamic API, screens can be conditionally defined by rendering Screen components conditionally. In the static API, the if property can be used to conditionally render screens based on a hook returning a boolean.
function RootStack() {
const isLoggedIn = useIsLoggedIn();
return (
<Stack.Navigator>
{isLoggedIn ? (
<Stack.Screen name="Home" component={HomeScreen} />
) : (
<Stack.Screen name="SignIn" component={SignInScreen} />
)}
</Stack.Navigator>
);
}
const useIsGuest = () => !useIsLoggedIn();
const RootStack = createNativeStackNavigator({
screens: {
Home: {
screen: HomeScreen,
if: useIsLoggedIn,
},
SignIn: {
screen: SignInScreen,
if: useIsGuest,
},
},
});
See Authentication flow for a complete example.
Nested navigators
In the dynamic API, a nested navigator is a component passed as the component prop of a screen. In the static API, it's a configuration object and not a component. The configuration object can be used the same way as a screen component in the static API:
const Tab = createBottomTabNavigator();
function HomeTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Groups" component={GroupsScreen} />
<Tab.Screen name="Chats" component={ChatsScreen} />
</Tab.Navigator>
);
}
const Stack = createNativeStackNavigator();
function RootStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeTabs} />
</Stack.Navigator>
);
}
const HomeTabs = createBottomTabNavigator({
screens: {
Groups: GroupsScreen,
Chats: ChatsScreen,
},
});
const RootStack = createNativeStackNavigator({
screens: {
Home: HomeTabs,
},
});
Groups
In the dynamic API, groups are rendered as <Stack.Group> elements wrapping screens. In the static API, they are defined in a groups object in the navigator config:
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Group screenOptions={{ presentation: 'modal' }}>
<Stack.Screen name="Settings" component={SettingsScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Group>
</Stack.Navigator>
const RootStack = createNativeStackNavigator({
screens: {
Home: HomeScreen,
},
groups: {
Modals: {
screenOptions: { presentation: 'modal' },
screens: {
Settings: SettingsScreen,
Profile: ProfileScreen,
},
},
},
});
Groups in the static API support the same if hook as individual screens, so you can conditionally render an entire group of screens based on auth state or other conditions.
In addition, if you were using navigationKey to remove or remount screens on auth state changes (e.g. navigationKey={isSignedIn ? 'user' : 'guest'}), the group name serves the same purpose in the static API. So if a screen is placed in 2 groups with different conditions, it'll be removed or remounted similarly to how it would be with navigationKey in the dynamic API.
Wrappers around navigators
In the dynamic API, it's possible to wrap the navigator component. For example, to add context providers or additional UI around the navigator. Since the navigator configuration is not a component in static API, this is not possible.
However, in many cases, it may be possible to achieve the same result by using the layout property:
function RootStack() {
return (
<SomeProvider>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</SomeProvider>
);
}
const RootStack = createNativeStackNavigator({
layout: ({ children }) => <SomeProvider>{children}</SomeProvider>,
screens: {
Home: HomeScreen,
},
});
There are some subtle differences between the two approaches. A layout is rendered as part of the navigator and has access to the navigator's state and options, whereas a wrapper component is outside the navigator.
This is also not possible if the wrapper component used props from the parent component. However, you can still achieve this by moving those props to React context.
Deep linking
For deep links and dynamic links, the dynamic API requires a linking configuration that maps navigation structure to path patterns. In the static API, deep linking is automatic by default and paths are generated based on the screen name and automatically. Custom path patterns can be defined on each screen's linking property:
const linking = {
config: {
screens: {
Home: '',
Profile: 'user/:userId',
Chat: 'chat/:chatId',
Settings: 'settings',
},
},
};
function App() {
return (
<NavigationContainer linking={linking}>
<RootStack />
</NavigationContainer>
);
}
const RootStack = createNativeStackNavigator({
screens: {
Home: {
screen: HomeScreen,
},
Profile: {
screen: ProfileScreen,
linking: 'user/:userId',
},
Chat: {
screen: ChatScreen,
linking: 'chat/:chatId',
},
Settings: {
screen: SettingsScreen,
},
},
});
const Navigation = createStaticNavigation(RootStack);
function App() {
return <Navigation />;
}
If automatic path generation is not desired, enabled: true can be used to use only explicitly defined paths, or enabled: false to disable linking entirely:
function App() {
return <Navigation linking={{ enabled: false }} />;
}
See Configuring links for more details.
TypeScript types
In the dynamic API, param types are defined in a separate RootStackParamList type and each screen component is annotated with a navigator-specific prop type. In the static API, param types are inferred from the linking config:
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
};
const RootStack = createNativeStackNavigator<RootStackParamList>();
type RootStackType = typeof RootStack;
declare module '@react-navigation/core' {
interface RootNavigator extends RootStackType {}
}
type ProfileScreenProps = NativeStackScreenProps<RootStackParamList, 'Profile'>;
function ProfileScreen({ route }: ProfileScreenProps) {
return <Text>{route.params.userId}</Text>;
}
function ProfileScreen() {
const route = useRoute('Profile');
return <Text>{route.params.userId}</Text>;
}
const RootStack = createNativeStackNavigator({
screens: {
Profile: createNativeStackScreen({
screen: ProfileScreen,
linking: {
path: 'user/:userId',
},
}),
},
});
type RootStackType = typeof RootStack;
declare module '@react-navigation/core' {
interface RootNavigator extends RootStackType {}
}
Here, the param type { userId: string } is automatically inferred from the path pattern. The useRoute('ScreenName') hook with the screen name can be used to get the correctly typed route object inside the screen component.
The module augmentation with the root navigator type is used for type inference for useNavigation, useRoute, Link etc.
Alternately, params can be typed with StaticScreenProps that annotates the props of the screen component, useful if deep linking is not being used:
import type { StaticScreenProps } from '@react-navigation/native';
function ProfileScreen({ route }: StaticScreenProps<{ userId: string }>) {
return <Text>{route.params.userId}</Text>;
}
See Configuring TypeScript for more details.
Mixing Static and Dynamic APIs
Sometimes it may not be possible to use the static API for everything. So we have made sure that you can combine static and dynamic configuration in the same app. For example, keeping a dynamic root navigator while migrating nested navigators to static, or vice versa.
See Mixing Static and Dynamic APIs for details.