Navigation lifecycle
If you're coming from a web background, you might expect that when navigating from route A to route B, A unmounts and remounts when you return. React Navigation works differently - this is driven by the more complex needs of mobile navigation.
Unlike web browsers, React Navigation doesn't unmount screens when navigating away. When you navigate from Home to Profile:
ProfilemountsHomestays mounted
When going back from Profile to Home:
ProfileunmountsHomeis not remounted, existing instance is shown
Similar behavior can be observed (in combination) with other navigators as well. Consider a tab navigator with two tabs, where each tab is a stack navigator:
- Static
- Dynamic
const HomeStack = createNativeStackNavigator({
screens: {
Home: createNativeStackScreen({
screen: HomeScreen,
}),
Details: createNativeStackScreen({
screen: DetailsScreen,
}),
},
});
const SettingsStack = createNativeStackNavigator({
screens: {
Settings: createNativeStackScreen({
screen: SettingsScreen,
}),
Profile: createNativeStackScreen({
screen: ProfileScreen,
}),
},
});
const MyTabs = createBottomTabNavigator({
screenOptions: {
headerShown: false,
},
screens: {
HomeStack: createBottomTabScreen({
screen: HomeStack,
options: { tabBarLabel: 'Home' },
}),
SettingsStack: createBottomTabScreen({
screen: SettingsStack,
options: { tabBarLabel: 'Settings' },
}),
},
});
const Stack = createNativeStackNavigator();
function HomeStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
);
}
const StackA = createNativeStackNavigator();
function SettingsStack() {
return (
<StackA.Navigator>
<StackA.Screen name="Settings" component={SettingsScreen} />
<StackA.Screen name="Profile" component={ProfileScreen} />
</StackA.Navigator>
);
}
const Tab = createBottomTabNavigator();
function MyTabs() {
return (
<Tab.Navigator
screenOptions={{
headerShown: false,
}}
>
<Tab.Screen
name="HomeStack"
component={HomeStack}
options={{ tabBarLabel: 'Home' }}
/>
<Tab.Screen
name="SettingsStack"
component={SettingsStack}
options={{ tabBarLabel: 'Settings' }}
/>
</Tab.Navigator>
);
}
We start on the HomeScreen and navigate to DetailsScreen. Then we use the tab bar to switch to the SettingsScreen and navigate to ProfileScreen. After this sequence of operations is done, all 4 of the screens are mounted! If you use the tab bar to switch back to the HomeStack, you'll notice you'll be presented with the DetailsScreen - the navigation state of the HomeStack has been preserved!
Lifecycle events
Now that we understand how React lifecycle methods work in React Navigation, let's answer an important question: "How do we find out that a user is leaving (blur) it or coming back to it (focus)?"
To detect when a screen gains or loses focus, we can listen to focus and blur events:
- Static
- Dynamic
function ProfileScreen() {
const navigation = useNavigation('Profile');
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
console.log('ProfileScreen focused');
});
return unsubscribe;
}, [navigation]);
React.useEffect(() => {
const unsubscribe = navigation.addListener('blur', () => {
console.log('ProfileScreen blurred');
});
return unsubscribe;
}, [navigation]);
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Profile Screen</Text>
</View>
);
}
function ProfileScreen() {
const navigation = useNavigation('Profile');
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
console.log('ProfileScreen focused');
});
return unsubscribe;
}, [navigation]);
React.useEffect(() => {
const unsubscribe = navigation.addListener('blur', () => {
console.log('ProfileScreen blurred');
});
return unsubscribe;
}, [navigation]);
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Profile Screen</Text>
</View>
);
}
See Navigation events for more details.
For performing side effects, we can use the useFocusEffect - it's like useEffect but ties to the navigation lifecycle -- it runs the effect when the screen comes into focus and cleans it up when the screen goes out of focus:
- Static
- Dynamic
import { useFocusEffect } from '@react-navigation/native';
function ProfileScreen() {
useFocusEffect(
React.useCallback(() => {
// Do something when the screen is focused
console.log('ProfileScreen focus effect');
return () => {
// Do something when the screen is unfocused
// Useful for cleanup functions
console.log('ProfileScreen focus effect cleanup');
};
}, [])
);
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Profile Screen</Text>
</View>
);
}
import { useFocusEffect } from '@react-navigation/native';
function ProfileScreen() {
useFocusEffect(
React.useCallback(() => {
// Do something when the screen is focused
console.log('ProfileScreen focus effect');
return () => {
// Do something when the screen is unfocused
// Useful for cleanup functions
console.log('ProfileScreen focus effect cleanup');
};
}, [])
);
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Profile Screen</Text>
</View>
);
}
To render different things based on whether the screen is focused, we can use the useIsFocused hook which returns a boolean indicating whether the screen is focused.
To know the focus state inside of an event listener, we can use the navigation.isFocused() method. Note that using this method doesn't trigger a re-render like the useIsFocused hook does, so it is not suitable for rendering different things based on focus state.
Inactive screens
Many navigators also have an inactiveBehavior option that lets you "pause" or "unmount" screens when they are inactive:
- Static
- Dynamic
const MyTabs = createBottomTabNavigator({
screenOptions: {
inactiveBehavior: 'pause',
},
screens: {
Home: createBottomTabScreen({
screen: HomeScreen,
}),
Profile: createBottomTabScreen({
screen: ProfileScreen,
}),
},
});
const Tab = createBottomTabNavigator();
function MyTabs() {
return (
<Tab.Navigator
screenOptions={{
inactiveBehavior: 'pause',
}}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}
Here, "inactive" and "unfocused" have different meanings:
- A screen becomes "unfocused" as soon as you navigate away from it
- A screen becomes "inactive" based on various factors, such as gestures, animations, and other interactions after it becomes unfocused - without guarantees on timing
- Preloaded screens don't become inactive until after the first time they become focused, so their effects can run to initialize the screen
- Focus and blur are part of navigation lifecycle, but "inactive" is an optimization mechanism
Paused screens
Paused screens internally use <Activity mode="hidden">. When a screen is paused, the following things happen:
- Effects are cleaned up (similar to when a component unmounts)
- Content stays rendered and the state is preserved
- Content can still re-render at a lower priority
This means event listeners, subscriptions, timers etc. get cleaned up. This reduces unnecessary re-renders and resource usage for paused screens.
Side effects from events can still run. For example, if you have a audio player that emits progress updates, audio will keep playing and progress updates will keep coming in even when the screen is paused. To avoid this, you need to use lifecycle events to pause the audio when the screen becomes unfocused.
Pausing screens is not a replacement for lifecycle events. Treat it as an optimization mechanism only. If you need guarantees on when things get cleaned up, use lifecycle events such as blur or useFocusEffect.
React doesn't provide a way to distinguish paused screens from unmounted screens, which presents some caveats:
- APIs such as
getRootStatewon't include state of navigators nested inside paused screens - When using state persistence, state of navigators nested inside paused screens won't be persisted
If you don't want this behavior, you can set inactiveBehavior to none to avoid pausing them:
- Static
- Dynamic
const MyTabs = createBottomTabNavigator({
screenOptions: {
inactiveBehavior: 'none',
},
screens: {
Home: createBottomTabScreen({
screen: HomeScreen,
}),
Profile: createBottomTabScreen({
screen: ProfileScreen,
}),
},
});
const Tab = createBottomTabNavigator();
function MyTabs() {
return (
<Tab.Navigator
screenOptions={{
inactiveBehavior: 'none',
}}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}
Unmounted screens
When inactiveBehavior is set to unmount, screens will be unmounted when they become inactive. This means their content will be unmounted and their local state will be lost.
This is primarily useful for stack navigators where the list of screens can grow. Unmounting inactive screens can help free up resources.
To unmount inactive screens, you can set inactiveBehavior to unmount:
- Static
- Dynamic
const MyStack = createNativeStackNavigator({
screenOptions: {
inactiveBehavior: 'unmount',
},
screens: {
Home: createNativeStackScreen({
screen: HomeScreen,
}),
Details: createNativeStackScreen({
screen: DetailsScreen,
}),
},
});
const Stack = createNativeStackNavigator();
function MyStack() {
return (
<Stack.Navigator
screenOptions={{
inactiveBehavior: 'unmount',
}}
>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
);
}
Also consider reworking your navigation flow to avoid growing the stack indefinitely if possible - e.g. by using pop: true option in navigate or using replace when relevant.
Screens containing nested navigators won't be unmounted even when inactiveBehavior is set to unmount, and will be paused instead. This avoids losing nested navigation state.
Summary
- Screens stay mounted when navigating away from them
- The
useFocusEffecthook is likeuseEffectbut tied to the navigation lifecycle instead of the component lifecycle - The
useIsFocusedhook andnavigation.isFocused()method can be used to determine if a screen is currently focused - The
focusandblurevents can be used to know when a screen gains or loses focus - The
inactiveBehavioroption can be used to "pause" screens when they are inactive, which cleans up effects but keeps the content rendered and state preserved