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:

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.