Tech, Mobile Apps, React Native, React Navigation

How to properly navigate in React Native

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.

How to properly navigate in React Native

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.

How to properly navigate in React Native

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.

Setting Things up

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.

Creating the Views

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.

That's It!

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.

Author image

by Calin Tamas

  • Cluj-Napoca

Have an app idea? It’s in good hands with us.

Contact us
Contact us