Authentication flows
Most apps require that a user authenticates in some way to have access to data associated with a user or other private content. Typically the flow will look like this:
- The user opens the app.
- The app loads some authentication state from encrypted persistent storage (for example,
SecureStore). - When the state has loaded, the user is presented with either authentication screens or the main app, depending on whether valid authentication state was loaded.
- When the user signs out, we clear the authentication state and send them back to authentication screens.
We say "authentication screens" because usually there is more than one. You may have a main screen with a username and password field, another for "forgot password", and another set for sign up.
What we need
We want the following behavior from our authentication flow:
- When the user is signed in, we want to show the main app screens and not the authentication-related screens.
- When the user is signed out, we want to show the authentication screens and not the main app screens.
- After the user goes through the authentication flow and signs in, we want to unmount all of the screens related to authentication, and when we press the hardware back button, we expect to not be able to go back to the authentication flow.
How it will work
We can configure different screens to be available based on some condition. For example, if the user is signed in, we want Home to be available. If the user is not signed in, we want SignIn to be available.
- Static
- Dynamic
const RootStack = createNativeStackNavigator({
screens: {
Home: {
if: useIsSignedIn,
screen: HomeScreen,
},
SignIn: {
if: useIsSignedOut,
screen: SignInScreen,
},
},
});
Here, for each screen, we have defined a condition using the if property which takes a hook. The hook returns a boolean value indicating whether the user is signed in or not. If the hook returns true, the screen will be available, otherwise it won't.
This means:
- When
useIsSignedInreturnstrue, React Navigation will only use theHomescreen, since it's the only screen matching the condition. - Similarly, when
useIsSignedOutreturnstrue, React Navigation will use theSignInscreen.
This makes it impossible to navigate to the Home when the user is not signed in, and to SignIn when the user is signed in.
When the values returned by useIsSignedIn and useIsSignedOut change, the screens matching the condition will change:
- Let's say, initially
useIsSignedOutreturnstrue. This means that theSignInscreen is shown. - After the user signs in, the return value of
useIsSignedInwill change totrueanduseIsSignedOutwill change tofalse, which means:- React Navigation will see that the
SignInscreen no longer matches the condition, so it will remove the screen. - Then it'll show the
Homescreen automatically because that's the first screen available whenuseIsSignedInreturnstrue.
- React Navigation will see that the
The order of the screens matters when there are multiple screens matching the condition. For example, if there are two screens matching useIsSignedIn, the first screen will be shown when the condition is true.
Define the hooks
To implement the useIsSignedIn and useIsSignedOut hooks, we can start by creating a context to store the authentication state. Let's call it SignInContext:
import * as React from 'react';
const SignInContext = React.createContext();
Then we can implement the useIsSignedIn and useIsSignedOut hooks as follows:
function useIsSignedIn() {
const isSignedIn = React.useContext(SignInContext);
return isSignedIn;
}
function useIsSignedOut() {
return !useIsSignedIn();
}
We'll discuss how to provide the context value later.
{isSignedIn ? (
<Stack.Screen name="Home" component={HomeScreen} />
) : (
<Stack.Screen name="SignIn" component={SignInScreen} />
)}
Here, we have conditionally defined the screens based on the value of isSignedIn.
This means:
- When
isSignedInistrue, React Navigation will only see theHomescreen, since it's the only screen defined based on the condition. - Similarly, when
isSignedInisfalse, React Navigation will only see theSignInscreen.
This makes it impossible to navigate to the Home when the user is not signed in, and to SignIn when the user is signed in.
When the value of isSignedIn changes, the screens defined based on the condition will change:
- Let's say, initially
isSignedInisfalse. This means that theSignInscreen is shown. - After the user signs in, the value of
isSignedInwill change totrue, which means:- React Navigation will see that the
SignInscreen is no longer defined, so it will remove the screen. - Then it'll show the
Homescreen automatically because that's the first screen defined whenisSignedInreturnstrue.
- React Navigation will see that the
The order of the screens matters when there are multiple screens matching the condition. For example, if there are two screens defined based on isSignedIn, the first screen will be shown when the condition is true.
Add more screens
For our case, let's say we have 3 screens:
SplashScreen- This will show a splash or loading screen when we're restoring the token.SignIn- This is the screen we show if the user isn't signed in already (we couldn't find a token).Home- This is the screen we show if the user is already signed in.
So our navigator will look like:
- Static
- Dynamic
const RootStack = createNativeStackNavigator({
screens: {
Home: {
if: useIsSignedIn,
screen: HomeScreen,
},
SignIn: {
if: useIsSignedOut,
screen: SignInScreen,
options: {
title: 'Sign in',
},
},
},
});
const Navigation = createStaticNavigation(RootStack);
const Stack = createNativeStackNavigator();
export default function App() {
const isSignedIn = true;
return (
<NavigationContainer>
<Stack.Navigator>
{isSignedIn ? (
<Stack.Screen name="Home" component={HomeScreen} />
) : (
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{
title: 'Sign in',
}}
/>
)}
</Stack.Navigator>
</NavigationContainer>
);
}
Notice how we have only defined the Home and SignIn screens here, and not the SplashScreen. The SplashScreen should be rendered before we render any navigators so that we don't render incorrect screens before we know whether the user is signed in or not.
When we use this in our component, it'd look something like this:
- Static
- Dynamic
if (isLoading) {
// We haven't finished checking for the token yet
return <SplashScreen />;
}
const isSignedIn = userToken != null;
return (
<SignInContext.Provider value={isSignedIn}>
<Navigation />
</SignInContext.Provider>
);
if (isLoading) {
// We haven't finished checking for the token yet
return <SplashScreen />;
}
const isSignedIn = userToken != null;
return (
<NavigationContainer>
<Stack.Navigator>
{isSignedIn ? (
<Stack.Screen name="Home" component={HomeScreen} />
) : (
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{
title: 'Sign in',
}}
/>
)}
</Stack.Navigator>
</NavigationContainer>
);
In the above snippet, isLoading means that we're still checking if we have a token. This can usually be done by checking if we have a token in SecureStore and validating the token.
Next, we're exposing the sign in status via the SignInContext so that it's available to the useIsSignedIn and useIsSignedOut hooks.
In the above example, we have one screen for each case. But you could also define multiple screens. For example, you probably want to define password reset, signup, etc screens as well when the user isn't signed in. Similarly for the screens accessible after sign in, you probably have more than one screen.
- Static
- Dynamic
We can use groups to define multiple screens:
const RootStack = createNativeStackNavigator({
screens: {
// Common screens
},
groups: {
SignedIn: {
if: useIsSignedIn,
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
},
SignedOut: {
if: useIsSignedOut,
screens: {
SignIn