In this article, we are going to walk through mounting and unmounting of navigation routes in React Native. An expected behavior of your app is that once the authentication condition is met, a new set of navigation routes are available only to logged-in users, while the other screens which were displayed before authentication is removed and can’t be returned to unless the user signs out of the application.
For security in your app, protected routes provide you with a way to only display certain information/content on your app to specific users, while restricting access from unauthorized persons.
We will be working with Expo for this project because it’ll help us focus on the problem at hand instead of worrying about a lot of setups. The exact same steps in this article could be followed for a bare React Native application.
You need some familiarity with JavaScript and React Native to follow through with this tutorial. Here are a few important things you should already be familiar with:
- Custom components in React Native (how to create components, receive, pass, and use props in a component). Read more.
- React Navigation. Read more.
- Stack Navigator in React Native. Read more.
- Basic Knowledge of React Native Core components (
<View/>,<Text/>, etc.). Read more. - React Native
AsyncStorage. Read more. - Context API. Read more.
Project Setup And Base Authentication
If you’re new to using expo and don’t know how to install expo, visit the official documentation. Once the installation is complete, go ahead to initialize a new React Native project with expo from our command prompt:
expo init navigation-project
You will be presented with some options to choose how you want the base setup to be:
In our case, let’s select the first option to set up our project as a blank document. Now, wait until the installation of the JavaScript dependencies is complete.
Once our app is set up, we can change our directory to our new project directory and open it in your favorite code editor. We need to install the library we will be using for AsyncStorage and our navigation libraries. Inside your folder directory in your terminal, paste the command above and choose a template (blank would work) to install our project dependencies.
Let’s look at what each of these dependencies is for:
- @react-native-community/async-storage
Like localStorage on the web, it is a React Native API for persisting data on a device in key-value pairs. - @react-native-community/masked-view, react-native-screens, react-native-gesture-handle
These dependencies are core utilities that are used by most navigators to create the navigation structure in the app. (Read more in Getting started with React Native navigation.) - @react-navigation/native
This is the dependency for React Native navigation. - @react-navigation/stack
This is the dependency for stack navigation in React Native.
npm install @react-native-community/async-storage @react-native-community/masked-view @react-navigation/native @react-navigation/stack react-native-screens react-native-gesture-handle
To start the application use expo start from the app directory in your terminal. Once the app is started, you can use the expo app from your mobile phone to scan the bar code and view the application, or if you have an android emulator/IOS simulator, you can open the app through them from the expo developer tool that opens up in your browser when you start an expo application. For the images examples in this article, we will be using Genymotions to see our result. Here’s what our final result will look like in Genymotions:
Folder Structures
Let us create our folder structure from the start so that it’s easier for us to work with it as we proceed:
We need two folders first:
- context
This folder will hold the context for our entire application as we will be working with Context API for global state management. - views
This folder will hold both the navigation folder and the views for different screens.
Go ahead and create the two folders in your project directory.
Inside the context folder, create a folder called authContext and create two file inside of the authContext folder:
- AuthContext.js,
- AuthState.js.
We will need these files when we start working with Context API.
Now go to the views folder we created and create two more folders inside of it, namely:
- navigation,
- screens.
Now, we are not yet finished, inside the screens folder, create these two more folders:
- postAuthScreens,
- preAuthScreens.
If you followed the folder setup correctly, this is how your folder structure should look like at the moment:
Creating Our First Screen
Now let’s create our first screen and call it the welcomeScreen.js inside the preAuthScreens folder.
preAuthScreens > welcomeScreen.js
Here’s the content of our welcomeScreen.js file:
import React from 'react';
import { View, Text, Button, StyleSheet, TextInput } from 'react-native'; const WelcomeScreen = () => { const onUserAuthentication = () => { console.log("User authentication button clicked") } return ( <View style={styles.container}> <Text style={styles.header}>Welcome to our App!</Text> <View> <TextInput style={styles.inputs} placeholder="Enter your email here.." /> <TextInput style={styles.inputs} secureTextEntry={true} placeholder="Enter your password here.." />
<Button title="AUTHENTICATE" onPress={onUserAuthentication} /> </View> </View> )
} const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, header: { fontSize: 25, fontWeight: 'bold', marginBottom: 30 }, inputs: { width: 300, height: 40, marginBottom: 10, borderWidth: 1, }
}) export default WelcomeScreen
Here’s what we did in the code block above:
First, we imported the things we need from the React Native library, namely, View, Text, Button, TextInput. Next, we created our functional component WelcomeScreen.
You’ll notice that we imported the StyleSheet from React Native and used it to define styles for our header and also our <TextInput />.
Lastly, we export the WelcomeScreen component at the bottom of the code.
Now that we are done with this, let’s get this component to function as expected by using the useState hook to store the values of the inputs and update their states anytime a change happens in the input fields. We will also bring import the useCallback hook from React as we will be needing it later to hold a function.
First, while we are still in the WelcomeScreen component, we need to import the useState and useCallback from React.
import React, { useState, useCallback } from 'react';
Now inside the WelcomeScreen functional component, let’s create the two states for the email and password respectively:
...
const WelcomeScreen = () => { const [email, setEmail] = useState('') const [password, setPassword] = useState('') return ( ... )
}
...
Next, we need to modify our <TextInput /> fields so that the get their value from their respective states and update their state when the value of the input is updated:
import React, { useState, useCallback } from 'react';
import { View, Text, Button, StyleSheet, TextInput } from 'react-native'; const WelcomeScreen = () => { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const onInputChange = (value, setState) => { setState(value); } return ( <View> ... <View> <TextInput style={styles.inputs} placeholder="Enter your email here.." value={email} onChangeText={(value) => onInputChange(value, setEmail)} /> <TextInput style={styles.inputs} secureTextEntry={true} placeholder="Enter your password here.." value={password} onChangeText={(value) => onInputChange(value, setPassword)} /> ... </View> </View> )
}
...
In the code above, here is what we did:
- We made the
valueof each of the text inputs to point to their respective states. - We added the
onChangeTexthandler to our text inputs. This fires up anytime a new value is entered or deleted from the input fields. - We called our
onInputChangefunction which accepts two arguments:- The current
valueis supplied by theonChangeTexthandler. - The setter of the state that should be updated (for the first input field we pass
setEmailand the second we passsetPassword. - Finally, we write our
onInputChangefunction, and our function does only one thing: It updates the respective states with the new value.
- The current
The next thing we need to work on is the onUserAuthentication() function with is called whenever the button for the form submission is clicked.
Ideally, the user must have already created an account and login will involve some backend logic of some sort to check that the user exists and then assign a token to the user. In our case, since we are not using any backend, we will create an object holding the correct user login detail, and then only authenticate a user when the values they enter matches our fixed values from the login object of email and password that we will create.
Here’s the code we need to do this:
... const correctAuthenticationDetails = { email: 'demouser@gmail.com', password: 'password'
}
const WelcomeScreen = () => { ... // This function gets called when the `AUTHENTICATE` button is clicked const onUserAuthentication = () => { if ( email !== correctAuthenticationDetails.email || password !== correctAuthenticationDetails.password ) { alert('The email or password is incorrect') return } // In here, we will handle what happens if the login details are // correct } ... return ( ... )
}
...
One of the first things you’ll notice in the code above is that we defined a correctAuthenticationDetails (which is an object that holds the correct login details we expect a user to supply) outside of the WelcomeScreen() functional component.
Next, we wrote the content of the onUserAuthentication() function and used a conditional statement to check if the email or password held in the respective states does not match the one we supplied in our object.
If you would like to see what we have done so far, import the WelcomeScreen component into your App.js like this:
Open the App.js file and put this replace the entire code with this:
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { View } from 'react-native';
import WelcomeScreen from './views/screens/preAuthScreens/welcomeScreen';
export default function App() { return ( <View> <StatusBar style="auto" /> <WelcomeScreen /> </View> );
}
Looking closely at the code above, you’ll see that what we did was import the WelcomeScreen component and then used it in the App() function.
Here’s what the result looks like of our WelcomeScreen looks like:
Now that we are done building the WelcomeScreen component, let’s move ahead and start working with Context API for managing our global state.
Why Context API?
Using Context API, we do not need to install any additional library into ReactJS, it is less stressful to set up, and is one of the most popular ways of handling global state in ReactJS. For lightweight state management, it is a good choice.
Creating Our Context
If you recall, we created a context folder earlier and created a subfolder inside of it called the authContext.
Now let’s navigate to the AuthContext.js file in the authContext folder and create our context:
context > authContext > AuthContext.js
import React, { createContext } from 'react';
const AuthContext = createContext();
export default AuthContext;
The AuthContext we just created holds the loading state value and the userToken state values. Currently, in the createContext we declared in the code-block above, we didn’t initialize any default values here so our context is currently undefined. An example value of the auth context could be {loading: false, userToken: 'abcd}
The AuthState.js file holds our Context API logic and their state values. Functions written here can be called from anywhere in our app and when they update values in state, it is updated globally also.
First, let’s bring in all the imports we will need in this file:
context > AuthContext > AuthState.js
import React, { useState } from 'react';
import AuthContext from './AuthContext';
import AsyncStorage from '@react-native-community/async-storage';
We imported the useState() hook from ReactJS to hold our states, we imported the AuthContext file we created above because this is where our empty context for authentication is initialized and we will need to use it as you’ll see later on while we progress, finally we import the AsyncStorage package (similar to localStorage for the web).
AsyncStorage is a React Native API that allows you to persist data offline over the device in a React Native application.
... const AuthState = (props) => { const [userToken, setUserToken] = useState(null); const [isLoading, setIsLoading] = useState(true); const onAuthentication = async() => { const USER_TOKEN = "drix1123q2" await AsyncStorage.setItem('user-token', USER_TOKEN); setUserToken(USER_TOKEN); console.warn("user has been authenticated!") } return ( <AuthContext.Provider value={{ onAuthentication, }} > {props.children} </AuthContext.Provider> )
}
export default AuthState;
In the code block above here’s what we did:
-
We declared two states for the
userTokenandisLoading. TheuserTokenstate will be used to store the token saved toAsyncStorage, while theisLoadingstate will be used to track the loading status (initially it is set totrue). We will find out more about the use of these two states as we proceed. -
Next, we wrote our
onAuthentication()function. This function is anasyncfunction that gets called when the login button is clicked from thewelcomeScreen.jsxfile. This function will only get called if the email and password the user has supplied matches the correct user detail object we provided. Usually what happens during authentication is that a token is generated for the user after the user is authenticated on the backend using a package like JWT, and this token is sent to the frontend. Since we are not going into all of that for this tutorial, we created a static token and kept it in a variable calledUSER_TOKEN. -
Next, we use the
awaitkeyword to set our user token to AsyncStorage with the nameuser-token. Theconsole.warn()statement is just used to check that everything went right, you can take it off whenever you like. -
Finally, we pass our
onAuthenticatedfunction as a value inside our<AuthContext.Provider>so that we can access and call the function from anywhere in our app.
screens > preAuth > welcomeScreen.js
First, import useContext from ReactJS and import the AuthContext from the AuthContext.js file.
import React, { useState, useContext } from 'react';
import AuthContext from '../../../context/authContext/AuthContext'
...
Now, inside the welcomeScreen() functional component, let’s use the context which we have created:
...
const WelcomeScreen = () => { const { onAuthentication } = useContext(AuthContext) const onUserAuthentication = () => { if ( email !== correctAuthenticationDetails.email || password !== correctAuthenticationDetails.password ) { alert('The email or password is incorrect') return } onAuthentication() } return ( ... )
}
...
In the above code block, we destructured the onAuthentication function from our AuthContext and then we called it inside our onUserAuthentication() function and removed the console.log() statement which was there before now.
Right now, this will throw an error because we don’t yet have access to the AuthContext. To use the AuthContext anywhere in your application, we need to wrap the top-level file in our app with the AuthState (in our case, it is the App.js file).
Go to the App.js file and replace the code there with this:
import React from 'react';
import WelcomeScreen from './views/screens/preAuthScreens/welcomeScreen';
import AuthState from './context/authContext/AuthState' export default function App() { return ( <AuthState> <WelcomeScreen /> </AuthState> );
}
We’ve come so far and we’re done with this section. Before we move into the next section where we set up our routing, let’s create a new screen. The screen we are about to create will be the HomeScreen.js file which is supposed to show up only after successful authentication.
Go to: screens > postAuth.
Create a new file called HomeScreen.js. Here’s the code for the HomeScreen.js file:
screens > postAuth > HomeScreen.js
import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native'; const HomeScreen = () => { const onLogout = () => { console.warn("Logout button cliked") } return ( <View style={styles.container}> <Text>Now you're authenticated! Welcome!</Text> <Button title="LOG OUT" onPress={onLogout} /> </View> )
} const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', },
}) export default HomeScreen
For now, the logout button has a dummy console.log() statement. Later on, we will create the logout functionality and pass it to the screen from our context.
Setting Up Our Routes
We need to create three (3) files inside our navigation folder:
- postAuthNavigator.js,
- preAuthNavigator.js,
- AppNavigator.js.
Once you’ve created these three files, navigate to the preAuthNaviagtor.js file you just created and write this:
navigation > preAuthNavigator.js
import React from "react";
import { createStackNavigator } from "@react-navigation/stack";
import WelcomeScreen from "../screens/preAuthScreens/welcomeScreen"; const PreAuthNavigator = () => { const { Navigator, Screen } = createStackNavigator(); return ( <Navigator initialRouteName="Welcome"> <Screen name="Welcome" component={WelcomeScreen} /> </Navigator> )
}
export default PreAuthNavigator;
In the file above, here’s what we did:
- We imported the
createStackNavigatorfrom the@react-navigation/stackwhich we are using for our stack navigation. ThecreateStackNavigatorProvides a way for your app to transition between screens where each new screen is placed on top of a stack. By default the stack navigator is configured to have the familiar iOS and Android look & feel: new screens slide in from the right on iOS, fade in from the bottom on Android. Click here if you want to learn more about the stack navigator in React Native. - We destructured
NavigatorandScreenfrom thecreateStackNavigator(). - In our return statement, we created our navigation with the
<Navigator/>and created our screen with the<Screen/>. this means that if we had multiple screens that can be accessed before authentication, we will have multiple<Screen/>tags here representing them. - Finally, we export our
PreAuthNavigatorcomponent.
Let us do a similar thing for the postAuthNavigator.js file.
navigation > postAuthNavigator.js
import React from "react";
import { createStackNavigator } from "@react-navigation/stack";
import HomeScreen from "../screens/postAuthScreens/HomeScreen";
const PostAuthNavigator = () => { const { Navigator, Screen} = createStackNavigator(); return ( <Navigator initialRouteName="Home"> <Screen name="Home" component={HomeScreen} /> </Navigator> )
}
export default PostAuthNavigator;
As we see in the code above, the only difference between the preAuthNavigator.js and the postAuthNavigator.js is the screen being rendered. While the first one takes the WelcomeScreen, the postAuthNavigator.js takes the HomeScreen.
To create our AppNavigator.js we need to create a few things.
Since the AppNavigator.js is where we will be switching and checking which route will be available for access by the user, we need several screens in place for this to work properly, let’s outline the things we need to create first:
- TransitionScreen.js
While the app decides which navigation it is going to mount, we want a transition screen to show up. Typically, the transition screen will be a loading spinner or any other custom animation chosen for the app, but in our case, we will use a basic<Text/>tag to displayloading…. checkAuthenticationStatus()
This function is what we will be calling to check the authentication status which will determine which navigation stack is going to be mounted. We will create this function in our context and use it in the Appnavigator.js.
Now, let’s go ahead and create our TransitionScreen.js file.
screens > TransitionScreen.js
import React from 'react';
import { Text, View } from 'react-native'; const TransitionScreen = () => { return ( <View> <Text>Loading...</Text> </View> )
} export default TransitionScreen
Our transition screen is just a simple screen that shows loading text. We will see where to use this as we proceed in this article.
Next, let us go to our AuthState.js and write our checkAuthenticationStatus():
context > authContext > AuthState.js
import React, { useState, useEffect } from 'react';
import AuthContext from './AuthContext';
import AsyncStorage from '@react-native-community/async-storage'; const AuthState = (props) => { const [userToken, setUserToken] = useState(null); const [isLoading, setIsLoading] = useState(true); ... useEffect(() => { checkAuthenticationStatus() }, []) const checkAuthenticationStatus = async () => { try { const returnedToken = await AsyncStorage.getItem('user-toke n'); setUserToken(returnedToken); console.warn('User token set to the state value) } catch(err){ console.warn(`Here's the error that occured while retrievin g token: ${err}`) } setIsLoading(false) } const onAuthentication = async() => { ... } return ( <AuthContext.Provider value={{ onAuthentication, userToken, isLoading, }} > {props.children} </AuthContext.Provider> )
}
export default AuthState;
In the code block above, we wrote the function checkAuthenticationStatus(). In our function, here’s what we are doing:
- We used the
awaitkeyword to get our token fromAsyncStorage. WithAsyncStorage, if there’s no token supplied, it returnsnull. Our initialuserTokenstate is set tonullalso. - We use the
setUserTokento set our returned value fromAsyncStorageas our newuserToken. If the returned value isnull, it means ouruserTokenremainsnull. - After the
try{}…catch(){}block, we setisLoadingto false because the function to check authentication status is complete. We’ll need the value ofisLoadingto know if we should still be displaying theTransitionScreenor not. It’s worth considering setting an error if there is an error retrieving the token so that we can show the user a “Retry” or “Try Again” button when the error is encountered. - Whenever
AuthStatemounts we want to check the authentication status, so we use theuseEffect()ReactJS hook to do this. We call ourcheckAuthenticationStatus()function inside theuseEffect()hook and set the value ofisLoadingtofalsewhen it is done. - Finally, we add our states to our
<AuthContext.Provider/>values so that we can access them from anywhere in our app covered by the Context API.
Now that we have our function, it is time to go back to our AppNavigator.js and write the code for mounting a particular stack navigator based on the authentication status:
navigation > AppNavigator.js
First, we will import all we need for our AppNavigator.js.
import React, { useEffect, useContext } from "react";
import PreAuthNavigator from "./preAuthNavigator";
import PostAuthNavigator from "./postAuthNavigator";
import { NavigationContainer } from "@react-navigation/native"
import { createStackNavigator } from "@react-navigation/stack";
import AuthContext from "../../context/authContext/AuthContext";
import TransitionScreen from "../screens/TransitionScreen";
Now that we have all our imports, let’s create the AppNavigator() function.
...
const AppNavigator = () => { } export default AppNavigator
Next, we will now go ahead to write the content of our AppNavigator() function:
import React, { useState, useEffect, useContext } from "react";
import PreAuthNavigator from "./preAuthNavigator";
import PostAuthNavigator from "./postAuthNavigator";
import { NavigationContainer } from "@react-navigation/native"
import { createStackNavigator } from "@react-navigation/stack";
import AuthContext from "../../context/authContext/AuthContext";
import TransitionScreen from "../screens/transition"; const AppNavigator = () => { const { Navigator, Screen } = createStackNavigator(); const authContext = useContext(AuthContext); const { userToken, isLoading } = authContext; if(isLoading) { return <TransitionScreen /> } return ( <NavigationContainer> <Navigator> { userToken == null ? ( <Screen name="PreAuth" component={PreAuthNavigator} options={{ header: () => null }} /> ) : ( <Screen name="PostAuth" component={PostAuthNavigator} options={{ header: () => null }} /> ) } </Navigator> </NavigationContainer> )
} export default AppNavigator
In the above block of code, here’s an outline of what we did:
- We created a stack navigator and destructured the
NavigatorandScreenfrom it. - We imported the
userTokenand theisLoadingfrom ourAuthContext - When the
AuthStatemounts, thecheckAuthenticationStatus()is called in theuseEffeccthook there. We use theifstatement to check ifisLoadingistrue, if it istruethe screen we return is our<TransitionScreen />which we created earlier because thecheckAuthenticationStatus()function is not yet complete. - Once our
checkAuthenticationStatus()is complete,isLoadingis set tofalseand we return our main Navigation components. - The
NavigationContainerwas imported from the@react-navigation/native. It is only used once in the main top-level navigator. Notice that we are not using this in the preAuthNavigator.js or the postAuthNavigator.js. - In our
AppNavigator(), we still create a stack navigator. If theuserTokengotten from our Context API isnull, we mount thePreAuthNavigator, if its value is something else (meaning that theAsyncStorage.getItem()in thecheckAuthenticationStatus()returned an actual value), then we mount thePostAuthNavigator. Our conditional rendering is done using the ternary operator.
Now we’ve set up our AppNavigator.js. Next, we need to pass our AppNavigator into our App.js file.
Let’s pass our AppNavigator into the App.js file:
App.js
...
import AppNavigator from './views/navigation/AppNavigator'; ...
return ( <AuthState> <AppNavigator /> </AuthState> );
Let’s now see what our app looks like at the moment:
Here’s what happens when you supply an incorrect credential while trying to log in:
Adding The Logout Functionality
At this point, our authentication and route selection process is complete. The only thing left for our app is to add the logout functionality.
The logout button is in the HomeScreen.js file. We passed an onLogout() function to the onPress attribute of the button. For now, we have a simple console.log() statement in our function, but in a little while that will change.
Now, let’s go to our AuthState.js and write the function for logout. This function simply clears the AsyncStorage where the user token is saved.
context > authContext > AuthState.js
...
const AuthState = (props) => { ... const userSignout = async() => { await AsyncStorage.removeItem('user-token'); setUserToken(null); } return ( ... )
} export default AuthState;
The userSignout() is an asynchronous function that removes the user-token from our AsyncStorage.
Now we need to call the userSignout() function in our HomeScreen.js any time the logout button is clicked on.
Let’s go to our HomeScreen.js and use ther userSignout() from our AuthContext.
screens > postAuthScreens > HomeScreen.js
import React, { useContext } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import AuthContext from '../../../context/authContext/AuthContext' const HomeScreen = () => { const { userSignout } = useContext(AuthContext) const onLogout = () => { userSignout() } return ( <View style={styles.container}> <Text>Now you're authenticated! Welcome!</Text> <Button title="LOG OUT" onPress={onLogout} /> </View> )
}
...
In the above code block we imported thee useContext hook from ReactJS, then we imported our AuthContext. Next, we destructured the userSignout function from our AuthContext and this userSignout() function is called in our onLogout() function.
Now whenever our logout button is clicked, the user token in our AsyncStorage is cleared.
Voila! our entire process is finished.
Here’s what happens when you press the back button after you’re logged in:
Here’s what happens when you press the back button after logging out:
Here are some different behaviors we notice when using this pattern in our navigation stack switching:
- You’ll notice that there was nowhere we needed to make use of
navigation.navigate()ornavigation.push()to go to another route after login. Once our state is updated with the user token, the navigation stack rendered is automatically changed. - Pressing the back button on your device after login is successful cannot take you back to the login page, instead, it closes the app entirely. This behavior is important because you don’t want the user to be able to return back to the login page except they log out of the app. The same thing applies to logging out — once the user logs out, they cannot use the back button to return to the
HomeScreenscreen, but instead, the app closes.
Conclusion
In many Apps, authentication is one of the most important parts because it confirms that the person trying to gain access to protected content has the right to access the information. Learning how to do it right is an important step in building a great, intuitive, and easy to use/navigate the application.
Building on top of this code, here are a few things you might consider adding:
Here are also some important resources I found that will enlighten you more about authentication, security and how to do it right:
