Skip to main content
Version: 8.x

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 if property.
  • React hooks cannot be used in options, listeners etc. However, React.use() can be used to read context values in options callback (though it may produce ESLint warnings since ESLint cannot detect that it runs during render), and you can use the theme parameter instead of useTheme().

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:

Dynamic API
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>
);
}
Static API
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.

Screens no longer receive the navigation object as a prop in the static API. It's necessary to use the useNavigation hook instead:

Dynamic API
function HomeScreen({ navigation }) {
return (
<Button
title="Go to profile"
onPress={() => navigation.navigate('Profile')}
/>
);
}
Static API
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.

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.

Dynamic API
<Stack.Navigator
initialRouteName="Home"
screenOptions={{
headerTintColor: 'white',
headerStyle: { backgroundColor: 'tomato' },
}}
>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
Static API
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:

Dynamic API
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={({ route }) => ({
title: route.params.userId,
})}
listeners={{
focus: () => console.log('focused'),
}}
getId={({ params }) => params.userId}
/>
Static API
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.

Dynamic API
function RootStack() {
const isLoggedIn = useIsLoggedIn();

return (
<Stack.Navigator>
{isLoggedIn ? (
<Stack.Screen name="Home" component={HomeScreen} />
) : (
<Stack.Screen name="SignIn" component={SignInScreen} />
)}
</Stack.Navigator>
);
}
Static API
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:

Dynamic 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>
);
}
Static API
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:

Dynamic API
<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>
Static API
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:

Dynamic API
function RootStack() {
return (
<SomeProvider>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</SomeProvider>
);
}
Static API
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:

Dynamic API
const linking = {
config: {
screens: {
Home: '',
Profile: 'user/:userId',
Chat: 'chat/:chatId',
Settings: 'settings',
},
},
};

function App() {
return (
<NavigationContainer linking={linking}>
<RootStack />
</NavigationContainer>
);
}
Static API
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:

Static API with linking disabled
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:

Dynamic API
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>;
}
Static API
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.