Search code examples
reactjsreact-nativebackreact-navigation-v5

React Navigation 5, block back navigation after login


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:

  • Placing a callback to React Native's 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).
  • Using 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.
  • Instead navigating directly to 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.
  • Combining navigation and reset calls in different ways, I have only managed to get JS angry and throw errors and exceptions at me.

Aaaaand... I have ran out of ideas. Does anyone have any suggestions ?


Solution

  • 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.