Building an Authentication Flow with React Navigation

Author

Spencer Carli

Developer, cat dad, and devout pizza lover. Teaching at React Native School and building apps with Handlebar Labs.

0

Updated: March 27, 2017

I’m asked fairly often about setting up an authentication flow with React Navigation. That’s what we’ll cover today — we’ll have two “layouts” a signed in layout with a TabNavigator and a signed out layout with a two screen StackNavigator.

I’ll be using React Native Elements to handle the UI for this app.

Overview

Our app is going to work like so…

When the user first opens the app we’ll show them the SignedOut layout where they can then log in (logging in will simply consist of pressing the login button, everything else will just be for looks). When logged in the SignedIn layout should replace the SignedOut layout. A user shouldn't be able to swipe or go back to the previous layout unless they log out.

If a user previously logged in then we should show them the SignedInlayout immediately when they come back to the app.

Upon logging out the SignedOut layout should then be visible again. Again, a user shouldn't be able to go back to the SignedIn layout unless they sign in.

null

Following Along

If you want to follow along, or check out the finalized code, you can do so by checking out the project on GitHub. To get started all the screens will be set up already and we’ll focus solely on the routing logic.

Approach

Thinking about the app overview we know we need two “layouts”. The SignedOut layout can be represented by a StackNavigator. The SignedIn layout will be represented by a TabNavigator (you could easily use a DrawerNavigator instead).

We also need a way to determine which initial layout is shown — we’ll handle that in the primary entry point of the app (app/index.js). To keep track of whether the user has previously signed in or not will be accomplished with AsyncStorage. When logging in we'll set a key to notate that.

Finally we need a root SwitchNavigator that we’ll use to change primary layouts.

Setting up the SignedOut Layout

First we’ll set up our SignedOut layout. In app/router.js create a new StackNavigator with the SignIn and SignUp screens.

app/router.js

import { StackNavigator } from 'react-navigation';

import SignUp from './screens/SignUp';
import SignIn from './screens/SignIn';

export const SignedOut = StackNavigator({
  SignUp: {
    screen: SignUp,
    navigationOptions: {
      title: 'Sign Up',
    },
  },
  SignIn: {
    screen: SignIn,
    navigationOptions: {
      title: 'Sign In',
    },
  },
});

You’ll also want to use this SignedOut component in the app entry point.

app/index.js

import React from 'react';
import { SignedOut } from './router';

export default class App extends React.Component {
  render() {
    return <SignedOut />;
  }
}

Finally, update SignUp.js to, when pressing the "Sign In" button, navigate to the Sign In screen.

app/screens/Signup.js

export default ({ navigation }) => (
  <View style={{ paddingVertical: 20 }}>
    <Card>
      {/* ... */}

      <Button
        buttonStyle={{ marginTop: 20 }}
        backgroundColor="transparent"
        textStyle={{ color: '#bcbec1' }}
        title="Sign In"
        onPress={() => navigation.navigate('SignIn')}
      />
    </Card>
  </View>
);

That should leave with something like this

null

Setting Up the SignedIn Layout

Now let’s set up the TabNavigator for the SignedIn layout.

app/router.js

import { StackNavigator, TabNavigator } from 'react-navigation';

// ...

import Home from './screens/Home';
import Profile from './screens/Profile';

export const SignedIn = TabNavigator({
  Home: {
    screen: Home,
    navigationOptions: {
      tabBarLabel: 'Home',
      tabBarIcon: ({ tintColor }) => (
        <FontAwesome name="home" size={30} color={tintColor} />
      ),
    },
  },
  Profile: {
    screen: Profile,
    navigationOptions: {
      tabBarLabel: 'Profile',
      tabBarIcon: ({ tintColor }) => (
        <FontAwesome name="user" size={30} color={tintColor} />
      ),
    },
  },
});

Then render that from the entry point

app/index.j

import React from 'react';
import { SignedOut, SignedIn } from './router';

export default class App extends React.Component {
  render() {
    return <SignedIn />;
  }
}

Leaving with the the following

null

Tracking Sign In State

Now that we’ve got our different layouts put together let’s first handle our login logic, which takes place in app/auth.js. This is purely for demonstration's sake - obviously you would want to hook into a real auth system in reality.

app/auth.js

import { AsyncStorage } from 'react-native';

export const USER_KEY = 'auth-demo-key';

export const onSignIn = () => AsyncStorage.setItem(USER_KEY, 'true');

export const onSignOut = () => AsyncStorage.removeItem(USER_KEY);

export const isSignedIn = () => {
  return new Promise((resolve, reject) => {
    AsyncStorage.getItem(USER_KEY)
      .then(res => {
        if (res !== null) {
          resolve(true);
        } else {
          resolve(false);
        }
      })
      .catch(err => reject(err));
  });
};

For the onSignIn and onSignOut functions I'm either writing to or removing from AsyncStorage. In the isSignedIn function I'm returning a promise and in that promise I check for the existence of the "USER_KEY" - if it exists (meaning we're logged in) I resolve true from the promise, otherwise I resolve false.

I created these abstractions to keep all of our login logic in one place.

We can then update the app/index.js to call and use this information in determining which layout to render.

app/index.js

import { isSignedIn } from './auth';

export default class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      signedIn: false,
      checkedSignIn: false,
    };
  }

  componentDidMount() {
    isSignedIn()
      .then(res => this.setState({ signedIn: res, checkedSignIn: true }))
      .catch(err => alert('An error occurred'));
  }

  render() {
    const { checkedSignIn, signedIn } = this.state;

    // If we haven't checked AsyncStorage yet, don't render anything (better ways to do this)
    if (!checkedSignIn) {
      return null;
    }

    if (signedIn) {
      return <SignedIn />;
    } else {
      return <SignedOut />;
    }
  }
}

In the componentDidMount I make a call to the isSignedIn function to check whether this user was previously logged in and then update the component state with that information. I also tell my component that we've checked and have gotten a response back from that function.

I then use the component state in my render method to determine what should be rendered. If we’re still waiting for a response from the function I render null so there aren’t any flashes of the wrong layout. The rest is self explanatory.

Since our Sign Up/In and Sign Out buttons are already calling the onSignInand onSignUp functions we can test this out. When pressing the button you won't notice anything but if you refresh the app you'll notice the new layout rendered.

null

Configuring the Root Navigator

We’re going to create a new SwitchNavigator for our root. However, we’re going to create a function that actually returns the new root navigator and we can change the initial route depending on the signed in state.

app/router.js

import {
  StackNavigator,
  TabNavigator,
  SwitchNavigator,
} from 'react-navigation';

export const createRootNavigator = (signedIn = false) => {
  return SwitchNavigator(
    {
      SignedIn: {
        screen: SignedIn,
      },
      SignedOut: {
        screen: SignedOut,
      },
    },
    {
      initialRouteName: signedIn ? 'SignedIn' : 'SignedOut',
    }
  );
};

You can see that my different “layouts” are the screens of this navigator . The big difference is that I’ve wrapped this in a function and I determine the initial route by the signedIn argument of the function.

I can then call it from my app entry point like so.

app/index.js

export default class App extends React.Component {
  ...

  render() {
    const { checkedSignIn, signedIn } = this.state;

    // If we haven't checked AsyncStorage yet, don't render anything (better ways to do this)
    if (!checkedSignIn) {
      return null;
    }

    const Layout = createRootNavigator(signedIn);
    return <Layout />;
  }
}

This will give the exact same experience as we had before, just using a navigator.

Now if I update the call to onSignIn in SignIn.js or SignUp.js to, upon resolving the promise returned, navigate to the SignedIn layout I get the following interaction.

null

The code for that looks like

app/screens/SignIn.js

export default ({ navigation }) => (
  <View style={{ paddingVertical: 20 }}>
    <Card>
      <FormLabel>Email</FormLabel>
      <FormInput placeholder="Email address..." />
      <FormLabel>Password</FormLabel>
      <FormInput secureTextEntry placeholder="Password..." />

      <Button
        buttonStyle={{ marginTop: 20 }}
        backgroundColor="#03A9F4"
        title="SIGN IN"
        onPress={() => {
          onSignIn().then(() => navigation.navigate('SignedIn')); // NEW LOGIC
        }}
      />
    </Card>
  </View>
);

Likewise we can do the opposite for Profile.js

app/screens/Profile.js

export default ({ navigation }) => (
  <View style={{ paddingVertical: 20 }}>
    <Card title="John Doe">
      <View
        style={{
          backgroundColor: '#bcbec1',
          alignItems: 'center',
          justifyContent: 'center',
          width: 80,
          height: 80,
          borderRadius: 40,
          alignSelf: 'center',
          marginBottom: 20,
        }}
      >
        <Text style={{ color: 'white', fontSize: 28 }}>JD</Text>
      </View>
      <Button
        backgroundColor="#03A9F4"
        title="SIGN OUT"
        onPress={() => onSignOut().then(() => navigation.navigate('SignedOut'))} // NEW LOGIC
      />
    </Card>
  </View>
);
null

One thing I want to note about this approach is that you’ve got to be intentional and very aware of an re-renders in the file you call the createRootNavigator because whenever it's called your nav state will be lost.

How are you handling the authentication flow when using React Navigation?