The year 2020 has seen a lot of changes in the React Native world:

  • community adoption of React Hooks in React Native apps has increased
  • the docs have a new domain
  • the popular navigation library react-navigation adopted a declarative and component-based approach to implement navigation in an app
  • react-native-firebase, the go-to package to use Firebase SDK, released its sixth version

In this tutorial, I am going to walk you through building a simple chat application that a user can log in to without credentials and straightaway enter a chat room using the anonymous sign-in method provided by Firebase.

The purpose of this tutorial is to get you familiar with all the latest updates in React Native world and its libraries like react-navigation and react-native-firebase that are used often. If you wish to add a new feature that is not covered in this tutorial, feel free to do that and follow along at your own pace.

Requirements

The following requirements will make sure you have a suitable development environment:

  • Node.js above 10.x.x installed on your local machine
  • JavaScript/ES6 basics
  • watchman the file watcher installed
  • react-native-cli installed through npm or access via npx

For a complete walkthrough on how you can set up a development environment for React Native, you can go through official documentation here.

Also, do note that the following tutorial is going to use the react-native version 0.61.5. Please make sure you are using a version of React Native above 0.60.x.

Getting started with the Crowdbotics App Builder

To generate a new React Native project you can use the react-native cli tool. Or, if you want to follow along, I am going to generate a new app using the Crowdbotics App Builder.

Register either using your GitHub credentials or your email. Once logged in, you can click the Create App button to create a new app. The next screen is going to prompt you as to what type of application you want to build. Choose Mobile App.

Enter the name of the application and click the button Create App. After you link your GitHub account from the dashboard, you are going to have access to the GitHub repository for the app. This repo generated uses the latest react-native version and comes with built-in components and complete examples that can be the base foundation for your next app.

You can now clone or download the GitHub repo that is generated by the Crowdbotics app building platform. Once you have access to the repo on your local development environment, make sure to navigate inside it. You will have to install the dependencies for the first time using the command yarn install. Then, to make it work on the iOS simulator/devices, make sure to install pods using the following commands from a terminal window.

# navigate inside iOS
cd ios/

# install pods
pod install

That's it. It's a three-step process. Now, let us get back to our tutorial.

Setting up navigation

To start, create a new React Native project and install the dependencies to set up and use the react-navigation library.

# create a new project
npx react-native init rnAnonChatApp

cd rnAnonChatApp

# install core navigation dependencies
yarn add @react-navigation/native @react-navigation/stack react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view

From React Native 0.60.x and higher, linking is automatic so you don't need to run react-native link.

To finalize the installation, on iOS, you have to install pods. (Note: Make sure you have Cocoapods installed.)

cd ios/ && pod install

# after pods are installed
cd ..

Similarly, on Android, open android/app/build.gradle and add the following two lines in the dependencies section:

implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha02'

Lastly, to save the app from crashing in a production environment, add the following line in index.js.

import 'react-native-gesture-handler'
import { AppRegistry } from 'react-native'
import App from './App'
import { name as appName } from './app.json'

AppRegistry.registerComponent(appName, () => App)

That's it for setting up the react-navigation library.

Adding vector icons

What is a mobile app without the use of icons? One famous library created by Oblador is called react-native-vector-icons. This library has a set of icons bundled from AntDesign, FontAwesome, Ionicons, MaterialIcons, and so on.

In this section, let us install that. Open up a terminal window and run the command yarn add react-native-vector-icons to install the library.

After the library is installed, for iOS, copy the following list of fonts inside ios/rnAnonChatApp/Info.plist.

<key>UIAppFonts</key>
<array>
  <string>AntDesign.ttf</string>
  <string>Entypo.ttf</string>
  <string>EvilIcons.ttf</string>
  <string>Feather.ttf</string>
  <string>FontAwesome.ttf</string>
  <string>FontAwesome5_Brands.ttf</string>
  <string>FontAwesome5_Regular.ttf</string>
  <string>FontAwesome5_Solid.ttf</string>
  <string>Foundation.ttf</string>
  <string>Ionicons.ttf</string>
  <string>MaterialIcons.ttf</string>
  <string>MaterialCommunityIcons.ttf</string>
  <string>SimpleLineIcons.ttf</string>
  <string>Octicons.ttf</string>
  <string>Zocial.ttf</string>
</array>

Then, open ios/Podfile and add the following:

'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'

Open a terminal window to install pods.

cd ios/ && pod install

# after pods are installed
cd ..

For Android, make sure you add the following in android/app/build.gradle:

apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"

That's it to set up a vector icons library in a React Native project.

Build two screens

Before you proceed to set up a navigation pattern in this app to switch between the different screens, let us create two different screens first.

Create a new file called src/screens/Login.js and add the following code snippet. For now, it going to display a message and a button. This screen is going to be displayed when the user isn't authenticated to enter the app and access a chat room.

// Login.js
import React from 'react'
import { View, StyleSheet, Text, TouchableOpacity } from 'react-native'
import Icon from 'react-native-vector-icons/Ionicons'

export default function Login() {
  // firebase login function later

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Welcome to 🔥 Chat App</Text>
      <TouchableOpacity
        style={styles.button}
        onPress={() => alert('Anonymous login')}>
        <Text style={styles.buttonText}>Enter Anonymously</Text>
        <Icon name='ios-lock' size={30} color='#cfdce0' />
      </TouchableOpacity>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#dee2eb'
  },
  title: {
    marginTop: 20,
    marginBottom: 30,
    fontSize: 28,
    fontWeight: '500'
  },
  button: {
    flexDirection: 'row',
    borderRadius: 30,
    marginTop: 10,
    marginBottom: 10,
    width: 300,
    height: 60,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#cf6152'
  },
  buttonText: {
    color: '#dee2eb',
    fontSize: 24,
    marginRight: 5
  }
})

Next, create another file in the same directory called ChatRoom.js with the following code snippet:

//ChatRoom.js
import React from 'react'
import { View, StyleSheet, Text } from 'react-native'

export default function ChatRoom() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>
        You haven't joined any chat rooms yet :'(
      </Text>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#dee2eb'
  },
  title: {
    marginTop: 20,
    marginBottom: 30,
    fontSize: 28,
    fontWeight: '500'
  }
})

For now, the above screen component is going to display a text message, but later it is going to contain many functionalities and features for the user to interact with.

Setting up two separate navigators

Start by creating a new directory src/navigation/ and inside it, two new files:

  • SignInStack.js
  • SignOutStack.js

Both of these files are self-explanatory by their names. Their functionality is going to contain screens related to the state of the app. For example, the SignInStack.js is going to have a stack navigator that has screen files (such as ChatRoom.js) that a user can only access after they are authenticated.

What is a stack navigator?

A Stack Navigator provides the React Native app with a way to transition between different screens, similar to how the navigation in a web browser works. It pushes or pops a screen when in the navigational state.

Now that you have an idea of what exactly a stack navigator is, understand what NavigationContainer and createStackNavigator do.

  • NavigationContainer is a component that manages the navigation tree. It also contains the navigation state and has to wrap all the navigator’s structure.
  • createStackNavigator is a function used to implement a stack navigation pattern. This function returns two React components: Screen and Navigator, which help us configure each component screen.

Open SignInStack.js and add the following code snippet:

import * as React from 'react'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import ChatRoom from '../screens/ChatRoom.js'

const Stack = createStackNavigator()

export default function SignInStack() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name='ChatRoom'
          component={ChatRoom}
          options={{ title: 'Chat Room' }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  )
}

In the above snippet, there are two required props with each Stack.Screen. The prop name refers to the name of the route, and the prop component specifies which screen to render at that particular route.

Similarly, in the file SignOutStack.js, add the following code snippet:

import * as React from 'react'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import Login from '../screens/Login'

const Stack = createStackNavigator()

export default function SignOutStack() {
  return (
    <NavigationContainer>
      <Stack.Navigator headerMode='none'>
        <Stack.Screen name='Login' component={Login} />
      </Stack.Navigator>
    </NavigationContainer>
  )
}

This is how navigators are defined declaratively using version 5 of react-navigation. It follows a more component-based approach, similar to that of react-router in web development.

Before we begin to define a custom authentication flow (since this version of react-navigation does not support a SwitchNavigator like previous versions), let us add the Firebase SDK. Using the Firebase functions, you can then easily implement an authentication flow to switch between the two stack navigators.

Adding Firebase SDK

If you have used react-native-firebase version 5 or below, you may have noticed that it was a monorepo that used to manage all Firebase dependencies from one module.

Version 6 brings you the option to only install those Firebase dependencies required for features that you want to use. For example, in the current app, you are going to start by adding the auth package. Also, do note that the core module @react-native-firebase/app is always required.

Open a terminal window to install this dependency.

yarn add @react-native-firebase/app

Adding Firebase credentials to your iOS app

The Firebase provides a GoogleService-Info.plist file that contains all the API keys as well as other credentials for iOS devices to authenticate the correct Firebase project.

To get the credentials, go to the Firebase console and create a new project. After that, from the dashboard screen of your Firebase project, open Project settings from the side menu.

Then, go to the Your apps section and click on the icon iOS to select the platform.

Firebase platform selection screen

Enter the application details and click on Register app.

Firebase app registration screen

Then download the GoogleService-Info.plist file as shown below.

Firebase plist download screen

Open Xcode, then open the file /ios/rnAnonChatApp.xcodeproj file. Right-click on your project name and choose the Add Files option, then select the file to add to this project.

Contextual menu in Xcode

Now, open ios/rnAnonChatApp/AppDelegate.m and add the following header.

#import <Firebase.h>

Within the didFinishLaunchingWithOptions method, add the following configure method:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    if ([FIRApp defaultApp] == nil) {
      [FIRApp configure];
    }

Open a terminal window to install pods.

cd ios/ && pod install

# after pods are installed
cd ..

Adding Firebase credentials to your Android app

Firebase provides a google-services.json file that contains all the API keys as well as other credentials for Android devices to authenticate the correct Firebase project.

Go to the Your apps section and click on the icon Android to select the platform.

Firebase platform selection screen

Then download the google-services.json file as shown below.

Firebase json download page

Now add the downloaded JSON file to your React Native project at the following location: /android/app/google-services.json.

After that, open android/build.gradle and add the following:

dependencies {
        // ...
        classpath 'com.google.gms:google-services:4.2.0'
    }

Next, open android/app/build.gradle file and at the very bottom of it, add the following:

apply plugin: 'com.google.gms.google-services'

Adding Firebase Anonymous Auth

To support the anonymous login feature in the current app, make sure you install the following package:

yarn add @react-native-firebase/auth

# Using iOS
cd ios/ && pod install

# After installing
cd ..

Now go back to the Firebase console of the project and navigate to the Authentication section from the side menu.

Firebase console side menu

Go to the second tab Sign-in method and enable the Anonymous sign-in provider.

Sign-in method tab beneath Authentication header
Toggle to enable anonymous sign-in

That's it! The app is now ready to allow the user to log in anonymously.

Setting up an authentication flow

To complete the navigation of the current React Native app, you have to set up the authentication flow. Create a new file src/navigation/AuthNavigator.js and make sure to import the following as well as both stack navigators created early in this tutorial.

import React, { useState, useEffect, createContext } from 'react'
import auth from '@react-native-firebase/auth'
import SignInStack from './SignInStack'
import SignOutStack from './SignOutStack'

Then, create an AuthContext that is going to expose the user data to only those screens when the user successfully logs in, that is, the screens that are part of the SignInStack navigator.

export const AuthContext = createContext(null)

Define the AuthNavigator functional component. Inside it, create two state variables, initializing and user. The state variable initializing is going to be true by default. It helps to keep track of the changes in the user state.

When the user state changes to authentication, the initializing variable is set to false. The change of the user's state is handled by a helper function called onAuthStateChanged.

Next, using the hook useEffect, subscribe to the auth state changes when the navigator component is mounted. On unmount, unsubscribe it.

Lastly, make sure to pass the value of user data using the AuthContext.Provider. Here is the complete snippet:

export default function AuthNavigator() {
  const [initializing, setInitializing] = useState(true)
  const [user, setUser] = useState(null)

  // Handle user state changes
  function onAuthStateChanged(result) {
    setUser(result)
    if (initializing) setInitializing(false)
  }

  useEffect(() => {
    const authSubscriber = auth().onAuthStateChanged(onAuthStateChanged)

    // unsubscribe on unmount
    return authSubscriber
  }, [])

  if (initializing) {
    return null
  }

  return user ? (
    <AuthContext.Provider value={user}>
      <SignInStack />
    </AuthContext.Provider>
  ) : (
    <SignOutStack />
  )
}

To make it work, open App.js and modify it as below:

import React from 'react'
import AuthNavigator from './src/navigation/AuthNavigator'

const App = () => {
  return <AuthNavigator />
}

export default App

Now, build the app for a specific mobile OS by running either of the commands mentioned:

npx react-native run-ios

# or

npx react-native run-android

Open up a simulator device, and you are going to be welcomed by the login screen.

Anonymous chat app welcome screen on mobile device

Adding SignIn functionality

Right now the app is in a state where you can use the Firebase authentication module to implement real-time signing in and signing out functionalities. Start with the screen/Login.js file to add sign-in functionality. Import the auth from @react-native-firebase/auth as shown below:

// rest of the import statements
import auth from '@react-native-firebase/auth'

Then, inside the Login functional component, define a helper method that will be triggered when the user presses the sign-in button on this screen. This helper method is going to be an asynchronous function.

async function login() {
  try {
    await auth().signInAnonymously()
  } catch (e) {
    switch (e.code) {
      case 'auth/operation-not-allowed':
        console.log('Enable anonymous in your firebase console.')
        break
      default:
        console.error(e)
        break
    }
  }
}

Lastly, add this method as the value of the prop onPress for TouchableOpacity.

<TouchableOpacity style={styles.button} onPress={login}>
  {/* ... */}
</TouchableOpacity>

Adding sign-out functionality

To add a sign out button, open navigation/SignInStack.js. This button is going to be represented by an icon on the right side of the header bar of the ChatRoom screen.

Start by importing Icon and auth statements.

// after rest of the import statements
import { TouchableOpacity } from 'react-native'
import Icon from 'react-native-vector-icons/Ionicons'
import auth from '@react-native-firebase/auth'

Then, add a logOut helper method that will be triggered when the user presses the icon. This method is going to be inside SignInStack.

async function logOut() {
  try {
    await auth().signOut()
  } catch (e) {
    console.error(e)
  }
}

Lastly, add headerRight in the options of Stack.Screen for ChatRoom.

<Stack.Screen
  name='ChatRoom'
  component={ChatRoom}
  options={{
    title: 'Chat Room',
    headerRight: () => (
      <TouchableOpacity style={{ marginRight: 10 }} onPress={logOut}>
        <Icon name='ios-log-out' size={30} color='#444' />
      </TouchableOpacity>
    )
  }}
/>

That's it. Now, go back to the device in which you are running this app and try logging into the app. Then using the icon in the header bar, log out of the app. This completes the authentication flow.

Successful anonymous app login

In the Firebase console, check that the user uid is created whenever a new user signs in the app and the identifier says anonymous.

Anonymous user list in Firebase

Add another screen

To enter the name of the chat room that is going to be saved in the database (which we will setup later using Firestore), create another screen called screens/CreateChatRoom.js.

This screen component is going to use a state variable called roomName to set the name of the room and store it in the database. The UI of the screen is going to be an input field as well as a button. The helper method handleButtonPress (as described below in code snippet) is going to manage the setting of a chat room. Later, you are going to add the business logic of saving the name.

import React, { useState } from 'react'
import {
  View,
  StyleSheet,
  Text,
  TextInput,
  TouchableOpacity
} from 'react-native'

export default function CreateChatRoom() {
  const [roomName, setRoomName] = useState('')

  function handleButtonPress() {
    if (roomName.length > 0) {
      // create new thread using firebase
    }
  }

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.textInput}
        placeholder='Thread Name'
        onChangeText={roomName => setRoomName(roomName)}
      />
      <TouchableOpacity style={styles.button} onPress={handleButtonPress}>
        <Text style={styles.buttonText}>Create chat room</Text>
      </TouchableOpacity>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#dee2eb'
  },
  title: {
    marginTop: 20,
    marginBottom: 30,
    fontSize: 28,
    fontWeight: '500'
  },
  button: {
    backgroundColor: '#2196F3',
    textAlign: 'center',
    alignSelf: 'center',
    paddingHorizontal: 40,
    paddingVertical: 10,
    borderRadius: 5,
    marginTop: 10
  },
  buttonText: {
    color: '#fff',
    fontSize: 18
  },
  textInput: {
    backgroundColor: '#fff',
    marginHorizontal: 20,
    fontSize: 18,
    paddingVertical: 10,
    paddingHorizontal: 10,
    borderColor: '#aaa',
    borderRadius: 10,
    borderWidth: 1,
    marginBottom: 5,
    width: 225
  }
})

Then, go the navigation/SignInStack.js file to add this newly created screen to the stack. Start by importing the screen itself.

import CreateChatRoom from '../screens/CreateChatRoom'

Next, add the Stack.Screen for CreateChatRoom in the stack as the second route in the navigator.

<Stack.Screen
  name='CreateChatRoom'
  component={CreateChatRoom}
  options={{
    title: 'Create a room'
  }}
/>

Lastly, to navigate to this new screen, using the navigation prop, add a headerLeft option in the screen ChatRoom as shown below. Make sure to convert the options to a function in order to use the navigation prop.

<Stack.Screen
  name='ChatRoom'
  component={ChatRoom}
  options={({ navigation }) => ({
    title: 'Chat Room',
    headerLeft: () => (
      <TouchableOpacity
        style={{ marginLeft: 10 }}
        onPress={() => navigation.navigate(CreateChatRoom)}>
        <Icon name='ios-add' size={30} color='#444' />
      </TouchableOpacity>
    ),
    headerRight: () => (
      <TouchableOpacity style={{ marginRight: 10 }} onPress={logOut}>
        <Icon name='ios-log-out' size={30} color='#444' />
      </TouchableOpacity>
    )
  })}
/>

Here is the output you are going to get:

Chat room creation and navigation is enabled

Conclusion

That's it for this part. By now you have learned how to set up a real-time authentication flow using Firebase and react-navigation and add an anonymous sign-in method.

In the next part, you are going to explore how to integrate Firestore to use a real-time database with this chat app, integrate react-native-gifted-chat to implement chat functionalities and UI, and create sub-collections in Firestore.