I am using React Navigation 5 in a project, and I'm having trouble trying to block a user from navigating back after a certain point.
The app uses a nested navigation structure similar to this:
ROOT (STACK)
|-- LoginScreens (STACK - options={{ gestureEnabled: false }} )
| |-- Login (SCREEN) -> when successful navigate to "Home"
| +-- Register (SCREEN) -> after registration, navigate to "Login"
|
+-- Home (TABS - options={{ gestureEnabled: false }} )
|-- BlahBlah (SCREEN)
|-- MyProfile (SCREEN)
+-- Dashboard (TABS)
|-- AllTasks (SCREEN)
+-- SomethingElse (SCREEN)
After a successful user login, the user is sent to the Home
screen and should not be able to navigate back to the LoginScreens
screen.
I have tried to use the componentDidMount
lifecycle method on Home
, as well as the useFocusEffect
hook, with the following:
BackHandler
, returning true from the handler works (true means back action has been handled, no further back handlers will be called), but it will also block any back navigation within the screens in Home
(e.g. I cannot navigate back from Dashboard to MyProfile).navigation.reset({ index: 1, routes: [{ name: "Home" }] })
. Without index: 1
the navigation just goes back to ROOT's initialRoute (in this case, LoginScreens
). With index: 1
, a Maximum update depth exceeded
error is thrown.Home
, I have tried using a navigation.reset()
(note: no params, clears the entire navigation history), and after that navigate to the Home
screen. This doesn't achieve the desired effect since the current route (ROOT's initialRoute, in this case: LoginScreens
) is still pushed on the navigation history before navigating to Home
.Aaaaand... I have ran out of ideas. Does anyone have any suggestions ?
Initially I had posted this solution: https://stackoverflow.com/a/60307042/12186963
However, eventually, I ended up not using it due to some serious jank issues I had with conditional rendering:
When aNavigator
/ Screen
mounts, a lot of stuff happens, multiple screens might get instantiated (especially if you're using tabbed navigators without lazy mount), nested <Navigator />
s might mount, react-navigation
has to re-evaluate it's state, and much, much more.
The app does not have a choice but hold on until the entire route tree mounts before rendering it, which can cause blank flashes between mounts. On lower-end devices, the blank screen can persist for longer times than a user would tolerate.
The better alternative solution that I have found involves the imperative call to NavigationContainer.resetRoot
method. By attaching a ref
to the NavigationContainer
, calling resetRoot
will always act on the root navigation state.
resetRoot
also allows to specify a new navigation state, which can be useful to change the currently active route.
The implementation is as follows:
libs/root-navigation.js
import React from "react";
// This is the ref to attach to the NavigationContainer instance
export const ref = React.createRef();
/**
* Resets the root navigation state, and changes the active route to the one specified
* @param {string} name The name of the route to navigate to after the reset
* @param {object|undefined} params Additional navigation params to pass to the route
*/
export function navigate(name, params) {
try {
ref.current.resetRoot({ index: 0, routes: [{ name, params }] });
} catch (e) {
console.error("Failed to reset the root navigation state. Make sure you have correctly attached the ref to the <NavigationContainer /> component.\nOriginal error:", e);
}
}
App.js
(or wherever you render your<NavigationContainer />
component:
import { NavigationContainer } from "@react-navigation/native";
import * as RootNavigation from "./libs/root-navigation";
import { createStackNavigator } from "@react-navigation/stack";
import LoginScreen from "./screens/Login";
import RegisterScreen from "./screens/Register";
import DashboardScreen from "./screens/Dashboard";
import AccountScreen from "./screens/Account";
const RootStack = createStackNavigator();
const AuthenticationStack = createStackNavigator();
const HomeStack = createStackNavigator();
function AuthenticationScreens() {
return <AuthenticationStack.Navigator initialRouteName="Login">
<AuthenticationStack.Screen name="Login" component={LoginScreen} />
<AuthenticationStack.Screen name="Register" component={RegisterScreen} />
</AuthenticationStack.Navigator>;
}
function HomeScreens() {
return <HomeStack.Navigator initialRouteName="Dashboard">
<HomeStack.Screen name="Dashboard" component={DashboardScreen} />
<HomeStack.Screen name="Account" component={AccountScreen} />
</HomeStack.Navigator>;
}
export default function MyApp() {
// ... your awesome code :)
return <NavigationContainer ref={RootNavigation.ref}>
<RootStack.Navigator initialRouteName="Authentication">
<RootStack.Screen name="Authentication" component={AuthenticationScreens} />
<RootStack.Screen name="Home" component={HomeScreens} />
</RootStack.Navigator>
</NavigationContainer>;
}
Then, in some other place in your app, you can always import the navigate()
function from the root-navigation.js
file, and use that to reset the root stack:
import { Pressable, Text, View } from "react-native";
import * as RootNavigation from "./libs/root-navigation";
import * as ServerAPI from "./server-api";
function LoginScreen() {
const email = "hello@world.com";
const password = "P@$sw0rD!";
const onLoginPress = () => {
ServerAPI.login(username, password).then(({ success, user })=>{
if (success === true) {
// Here we reset the root navigation state, and navigate to the "Home" screen
RootNavigation.navigate("Home", { user });
} else {
alert("Wrong email or password...");
}
});
}
return <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<Pressable onPress={onLoginPress}>
<Text>Login now!</Text>
</Pressable>
</View>;
}
I much more prefer this solution rather than my initial one. It also works with react-navigation@6.x
.