Most mobile apps follow the same old navigation pattern. The app is divided into two sides: a logged out side and a logged in side.
A user that has not been authenticated should not be able to get to the logged in side, while a logged in user should only be able to navigate within the logged in side. Simple mechanism.
Yet there has been some struggle to implement it correctly. I've encountered solutions that involve nesting the TabNavigator
into a StackNavigator
and making sure the user has no way of going back to the logged out routes when a login is successful. Another way of trying to implement the desired behavior is by removing the StackNavigator
altogether and using Modal Views that show up whenever the user is not authenticated. Unfortunately, these solutions are far more trickier to maintain than expected and add an unneeded level of complexity.
Luckily, we don't have to do that.
The React Native community has come up with a few navigation solutions, each possessing its own up and downs. We're going to implement the previously mentioned mechanism using React Navigation, which has recently reached a more mature version.
Our dependency stack from package.json
looks like this:
"dependencies": {
"@around25/jwt-utils": "^1.0.1",
"react": "16.3.1",
"react-native": "0.55.4",
"react-navigation": "^2.0.1"
}
Make sure to follow the guide using these package versions, as breaking changes may always arise.
The focus on this section will be on the navigator
folder. That is going to be the core of our React Native navigation.
├── src
| ├── api
| | └── auth.js
| ├── components
| | ├── Login
| | | └── index.js
| | ├── Main
| | | ├── Dashboard
| | | | └── index.js
| | | ├── Profile
| | | | └── index.js
| ├── navigator
| | ├── LoggedOut.js
| | ├── LoggedIn.js
| | └── index.js
| └── Root.js
└── index.js // app entry point
Change the default entry point to our app to use the Root.js
component.
// index.js
import { AppRegistry } from 'react-native'
import Root from './src/Root'
AppRegistry.registerComponent('ReactNavigationExample', () => Root);
For now, Root
can just render an empty view.
The component that perfectly fits our needs is createSwitchNavigator, allowing us to separate our app into two sides. In navigator/index.js
we are going to configure our two navigators: the LoggedOutNavigator
and the LoggedInNavigator
. The trick here is to write a function that takes the loggedIn
state as its single parameter and returns the SwitchNavigator
with the initialRouteName
corresponding to the state.
// src/navigator/index.js
import { createSwitchNavigator } from 'react-navigation'
import LoggedOutNavigator from './LoggedOut'
import LoggedInNavigator from './LoggedIn'
export const getRootNavigator = (loggedIn = false) => createSwitchNavigator(
{
LoggedOut: {
screen: LoggedOutNavigator
},
LoggedIn: {
screen: LoggedInNavigator
}
},
{
initialRouteName: loggedIn ? 'LoggedIn' : 'LoggedOut'
}
);
There's a StackNavigator for the logged out side, containing only the Login
component.
// src/navigator/LoggedOut.js
import { createStackNavigator } from 'react-navigation'
import Login from '../components/Login'
const LoggedOutNavigator = createStackNavigator({
Login: {
screen: Login
}
});
export default LoggedOutNavigator
On the logged in side, a TabNavigator with two tabs (a Dashboard
and a Profile
) is created.
// src/navigator/LoggedIn.js
import { createBottomTabNavigator } from 'react-navigation'
import Dashboard from '../components/Main/Dashboard'
import Profile from '../components/Main/Profile'
const LoggedInNavigator = createBottomTabNavigator({
Dashboard: {
screen: Dashboard
},
Profile: {
screen: Profile
}
});
export default LoggedInNavigator
Now that the routes are set up and we have the getRootNavigator
function that returns the right Navigator
, time to move on to writing the components.
We need four main components: Root
, Login
, Dashboard
and Profile
. The last two are for demo purposes only. Having a TabNavigator
with only one Tab is pretty boring.
Root
must first render a loading
state, while checking if a user is authenticated. When the verification is finished, the loggedIn
state is updated and the corresponding Navigator
is rendered.
// src/Root.js
import React, { Component } from 'react'
import { View, ActivityIndicator, StyleSheet } from 'react-native'
import { getRootNavigator } from './navigator'
import { isLoggedIn } from './api/auth'
export default class Root extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
loggedIn: false
};
}
async componentDidMount() {
const loggedIn = await isLoggedIn();
this.setState({ loggedIn, loading: false });
}
render() {
if (this.state.loading) {
return (
<View style={styles.base}>
<ActivityIndicator size='small' />
</View>
)
}
const RootNavigator = getRootNavigator(this.state.loggedIn);
return <RootNavigator />
}
}
const styles = StyleSheet.create({
base: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
})
For demo purposes, we're going to write the isLoggedIn()
function only to check if there's a token set in AsyncStorage
. In order to handle token storage and retrieval with as less boilerplate code as possible, I'm using a small library I wrote this week.
// src/api/auth.js
import { AsyncStorage } from 'react-native'
import TokenService from '@around25/jwt-utils'
const Token = new TokenService({
storageSystem: AsyncStorage
});
const isLoggedIn = async () => {
const tok = await Token.get();
return !!tok
}
That's it. The mechanism is in place. The last thing we need to make sure is that the onLogin
method from the Login
component performs the authentication and then redirects to the correct View
.
// src/components/Login/index.js
import { login } from '../../api/auth'
export default class Login extends Component {
constructor(props) {
super(props);
this.onLogin = this.onLogin.bind(this);
}
async onLogin() {
await login();
this.props.navigation.navigate('Dashboard');
}
render() {
return (
<View style={styles.base}>
<Button
title='Login'
onPress={this.onLogin}/>
</View>
);
}
}
const styles = StyleSheet.create({
base: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
})
The login
method from auth.js
is responsible for making the API call to retrieve an access token and setting that token in the AsyncStorage
.
// src/api/auth.js
const login = () => {
// Make API call to retrieve an access token
const tok = 'this_is_a_demo_access_token';
return Token.store(tok);
}
Likewise, a logout
function should remove the token.
// src/api/auth.js
const logout = () => {
return Token.remove();
}
The onLogout
method is similar to onLogin
. It calls logout()
from api/auth.js
and redirects to the Login
view.
Now you know to properly navigate in React Native. We have a functional authentication flow. By separating the app into these two sides, we make sure that, depending on the its auth state, the user is kept on the right side.
It is also easier to maintain and extend each side, as they don't depend on each other. The logged out side could have a register flow, or a more complex authentication process. Either way, a good separation of concerns is achieved.
The complete source code is available on our GitHub.