Updated May 24, 2017
Building an Authentication Flow with React Navigation
Originally publish on medium.com.
Hey - React Navigation has a new API! To read an updated article please check out the new tutorial.
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 SignedIn
layout 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.
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
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
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 onSignIn
and 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.
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.
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>
);
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?