Skip to main content

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

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.

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 useIsSignedIn returns true, React Navigation will only use the Home screen, since it's the only screen matching the condition.
  • Similarly, when useIsSignedOut returns true, React Navigation will use the SignIn screen.

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 useIsSignedOut returns true. This means that the SignIn screen is shown.
  • After the user signs in, the return value of useIsSignedIn will change to true and useIsSignedOut will change to false, which means:
    • React Navigation will see that the SignIn screen no longer matches the condition, so it will remove the screen.
    • Then it'll show the Home screen automatically because that's the first screen available when useIsSignedIn returns true.

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.

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:

const RootStack = createNativeStackNavigator({
screens: {
Home: {
if: useIsSignedIn,
screen: HomeScreen,
},
SignIn: {
if: useIsSignedOut,
screen: SignInScreen,
options: {
title: 'Sign in',
},
},
},
});

const Navigation = createStaticNavigation(RootStack);

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:

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>
);

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.

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