Build a Chat App with Firebase and React Native

Aman Mittal - Apr 26 '19 - - Dev Community

Last updated on: November 26th, 2021

In this tutorial, you are going to build a chat application using React Native, Expo, and Firebase as the backend service. The application will contain a simple login system using an email address for each specific user. The user will be allowed to upload a profile picture. The chat application will be more of a global chat room but works in real-time.

You can find the complete source code for this tutorial at this GitHub Repository.

Installing Dependencies

To get started, you must have Expo CLI installed on your local machine. Run the following commands from your terminal in order to install the CLI and generate a new project using it.

# To install expo-cli
npm install -g expo-cli

# To generate new project
expo init RNfirebase-chat

# Choose blank template when asked

# traverse inside the project directory
cd RNfirebase-chat
Enter fullscreen mode Exit fullscreen mode

Once the project is generated, you can run it in an iOS simulator or an Android emulator to verify that everything works. Android developers should make sure that an Android Virtual Device is running before executing the command below.

# for iOS simalulor
yarn ios

# for Android device/emulator
yarn android
Enter fullscreen mode Exit fullscreen mode

Next, install a dependency called react-native-gifted-chat that provides a customizable UI for a chat application. For navigating between different screens, we are going to use react-navigation and lastly, to connect with the Firebase project, we need Firebase SDK.

npm install @react-navigation/native @react-navigation/stack react-native-gifted-chat

# OR is using yarn 

yarn add @react-navigation/native @react-navigation/stack react-native-gifted-chat

# after the above dependencies install successfully 

expo install firebase expo-constants dotenv react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
Enter fullscreen mode Exit fullscreen mode

In order to build the application, we are going to need:

  • A user authentication service
  • A service to store the user's email
  • A service to store messages

All of these services are going to be leveraged from Firebase. When building an authentication flow, we will not go into covering the depths of implementing Firebase Auth with Expo. We have already covered that in-depth in a separate tutorial here.

Setting up Firebase

Firebase is an application development tool by Google that provides an SDK with services like email and social media authentication, real-time database, machine learning kit, APIs, and so on. Firebase can be integrated with a cloud service, Google Cloud Platform.

In the application, we are going to use email authentication and cloud storage. To set up a Firebase free tier project, visit the Firebase Console and create a new project, enter a name and then click the button Add Project button.

setting up firebase

Next, add the name of the new Firebase project and then click Continue. When asked for the Google Analytics setup, you can disable it as it won't be used in this example. Then click Create Project.

Once the Firebase project is created, you will be welcomed by the home screen like below.

create firebase project

Take a look at the side menu bar on the left. This is the main navigation in any Firebase project. First, we need to enable authentication. Click on the Authentication tab under the Build section, then click on the Sign-in method. Enable authentication using Email/Password and then hit the Save button.

enable authentication

On the Dashboard screen, in the left side menu, click the settings icon, and then go to the Project Settings page and then look for the section General > Your apps. If it's a new project, there won't be any apps.

project settings

Click the Web button. It will prompt you to enter the details of your app. Enter the app’s nickname, and then click the Register app button.

register app in firebase

Then, Firebase will provide configuration objects with API keys and other keys that are required to use different Firebase services.

API for firebase

These API keys can be included in your React Native app as they are not used to access Firebase services’ backend resources. That can only be done by Firebase security rules.
This does not mean that you should expose these keys to a version control host such as GitHub.
In the post How to integrate Firebase Authentication with an Expo app we discussed how to setup environment variables in .env and use them using the expo-constants package. We will follow the same methodology here.

Create a .env file at the root of your React Native project add the following. Replace the X’s with your actual keys from Firebase.

API_KEY=XXXX
AUTH_DOMAIN=XXXX
PROJECT_ID=XXXX
STORAGE_BUCKET=XXXX
MESSAGING_SENDER_ID=XXXX
APP_ID=XXX
Enter fullscreen mode Exit fullscreen mode

Next, rename the app.json file to app.config.js at the root of your project. Add the import statement to read the environment variables using the dotenv configuration. Since it's a JavaScript file, you will have to export all Expo configuration variables and also add an extra object that contains Firebase configuration keys. Here is how the file should look like after this step:

import 'dotenv/config';

export default {
  expo: {
    name: 'expo-firebase-auth-example',
    slug: 'expo-firebase-auth-example',
    version: '1.0.0',
    orientation: 'portrait',
    icon: './assets/icon.png',
    splash: {
      image: './assets/splash.png',
      resizeMode: 'contain',
      backgroundColor: '#ffffff'
    },
    updates: {
      fallbackToCacheTimeout: 0
    },
    assetBundlePatterns: ['**/*'],
    ios: {
      supportsTablet: true
    },
    android: {
      adaptiveIcon: {
        foregroundImage: './assets/adaptive-icon.png',
        backgroundColor: '#FFFFFF'
      }
    },
    web: {
      favicon: './assets/favicon.png'
    },
    extra: {
      apiKey: process.env.API_KEY,
      authDomain: process.env.AUTH_DOMAIN,
      projectId: process.env.PROJECT_ID,
      storageBucket: process.env.STORAGE_BUCKET,
      messagingSenderId: process.env.MESSAGING_SENDER_ID,
      appId: process.env.APP_ID
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Now, all the keys inside the extra object are readable app-wide using expo-constants. This package allows reading values from app.json - or in this case, the app.config.js file.

Inside your React Native project, create a new directory at the root called config/ and add a file called firebase.js. Edit the file as shown below:

import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import Constants from 'expo-constants';

// Firebase config
const firebaseConfig = {
  apiKey: Constants.manifest.extra.apiKey,
  authDomain: Constants.manifest.extra.authDomain,
  projectId: Constants.manifest.extra.projectId,
  storageBucket: Constants.manifest.extra.storageBucket,
  messagingSenderId: Constants.manifest.extra.messagingSenderId,
  appId: Constants.manifest.extra.appId,
  databaseURL: Constants.manifest.extra.databaseURL
};

// initialize firebase
initializeApp(firebaseConfig);

export const auth = getAuth();
export const database = getFirestore();

Enter fullscreen mode Exit fullscreen mode

Setup Firestore Database

The next step is to enable the Database rules. Visit the second tab called Firestore Database from the sidebar menu.

firestore database

Click Create Database. When asked for security rules, select test mode for this example. You can learn more about Security Rules with Firebase here and later on, update your rules accordingly.

firebase database settings

Next, let the location be the default and click Enable.

That's it for the setup part. In the next section, let’s start building the application.

Chat Screen

The react-native-gifted-chat component allows us to display chat messages that are going to be sent by different users. To get started, create a new directory called screens. This is where we are going to store all of the screen components. Inside this directory, create a new file, Chat.js with the following code snippet.

import React from 'react'
import { GiftedChat } from 'react-native-gifted-chat'

export default function Chat() {
  return (
    <GiftedChat />
  )
}

Enter fullscreen mode Exit fullscreen mode

Now open the App.js file and add logic to create a navigational component using the react-navigation module. This file will contain a RootNavigator, a ChatStack navigator that contains only one screen, and later we will add an AuthStack navigator with business logic to handle authenticated users to only view chat screen.

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

import Chat from './screens/Chat';

const Stack = createStackNavigator();

function ChatStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name='Chat' component={Chat} />
    </Stack.Navigator>
  );
}

function RootNavigator() {
  return (
    <NavigationContainer>
      <ChatStack />
    </NavigationContainer>
  );
}

export default function App() {
  return <RootNavigator />;
}
Enter fullscreen mode Exit fullscreen mode

Now if you run the simulator device, you will notice that there is a bare minimum chat screen that has a plain white header, background, and at the bottom of the screen, an input area where the user can enter the message. When typing something, a Send button automatically appears.

send button

However, this Send button does not have any functionality right now.

Protect your Code with Jscrambler

Adding a Login Screen

Create a screen component called Login.js inside the screens/ directory. This component file will contain the structure of components on the Login screen.

The screen itself contains two input fields for the app user to enter their credentials and a button to Login into the app. Another button is provided to navigate to the sign-up screen in case the user has not registered with the app. All of these components are created using React Native.

Start by importing the necessary components from React Native core and auth object from config/firebase.js file.

The onHandleLogin method is going to authenticate a user's credentials using signInWithEmailAndPassword() method from Firebase Auth. If the credentials are accurate, the user will navigate to the Chat screen. If not, there will be some error shown in your terminal window. You can add your own business logic to handle these errors.

Here is the complete code snippet for the Login.js file:

import React, { useState } from 'react';
import { StyleSheet, Text, View, Button, TextInput } from 'react-native';
import { signInWithEmailAndPassword } from 'firebase/auth';
import { auth } from '../config/firebase';

export default function Login({ navigation }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const onHandleLogin = () => {
    if (email !== '' && password !== '') {
     signInWithEmailAndPassword(auth, email, password)
        .then(() => console.log('Login success'))
        .catch(err => console.log(`Login err: ${err}`));
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Welcome back!</Text>
      <TextInput
        style={styles.input}
        placeholder='Enter email'
        autoCapitalize='none'
        keyboardType='email-address'
        textContentType='emailAddress'
        autoFocus={true}
        value={email}
        onChangeText={text => setEmail(text)}
      />
      <TextInput
        style={styles.input}
        placeholder='Enter password'
        autoCapitalize='none'
        autoCorrect={false}
        secureTextEntry={true}
        textContentType='password'
        value={password}
        onChangeText={text => setPassword(text)}
      />
      <Button onPress={onHandleLogin} color='#f57c00' title='Login' />
      <Button
        onPress={() => navigation.navigate('Signup')}
        title='Go to Signup'
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    paddingTop: 50,
    paddingHorizontal: 12
  },
  title: {
    fontSize: 24,
    fontWeight: '600',
    color: '#444',
    alignSelf: 'center',
    paddingBottom: 24
  },
  input: {
    backgroundColor: '#fff',
    marginBottom: 20,
    fontSize: 16,
    borderWidth: 1,
    borderColor: '#333',
    borderRadius: 8,
    padding: 12
  }
});
Enter fullscreen mode Exit fullscreen mode

Here is how the screen will look like:

login screen

Creating the Signup Screen

The Signup screen is similar to the Login one. It has exactly the same input fields and button with only one exception. The handler method defined in this file called onHandleSignup uses createUserWithEmailAndPassword() method from Firebase to create a new user account.

Create a new file inside the screens directory and let's name it Signup.js. Add the following code snippet:

import React, { useState } from 'react';
import { StyleSheet, Text, View, Button, TextInput } from 'react-native';
import { createUserWithEmailAndPassword } from 'firebase/auth';
import { auth } from '../config/firebase';

export default function Signup({ navigation }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const onHandleSignup = () => {
    if (email !== '' && password !== '') {
  createUserWithEmailAndPassword(auth, email, password)
        .then(() => console.log('Signup success'))
        .catch(err => console.log(`Login err: ${err}`));
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Create new account</Text>
      <TextInput
        style={styles.input}
        placeholder='Enter email'
        autoCapitalize='none'
        keyboardType='email-address'
        textContentType='emailAddress'
        value={email}
        onChangeText={text => setEmail(text)}
      />
      <TextInput
        style={styles.input}
        placeholder='Enter password'
        autoCapitalize='none'
        autoCorrect={false}
        secureTextEntry={true}
        textContentType='password'
        value={password}
        onChangeText={text => setPassword(text)}
      />
      <Button onPress={onHandleSignup} color='#f57c00' title='Signup' />
      <Button
        onPress={() => navigation.navigate('Login')}
        title='Go to Login'
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    paddingTop: 50,
    paddingHorizontal: 12
  },
  title: {
    fontSize: 24,
    fontWeight: '600',
    color: '#444',
    alignSelf: 'center',
    paddingBottom: 24
  },
  input: {
    backgroundColor: '#fff',
    marginBottom: 20,
    fontSize: 16,
    borderWidth: 1,
    borderColor: '#333',
    borderRadius: 8,
    padding: 12
  }
});
Enter fullscreen mode Exit fullscreen mode

Here is how the screen will look like:

signup screen

Adding authenticated user provider

In Reactjs, the Context API is designed to share data that is considered global for a tree of React components. When you are creating a context there is a requirement to pass a default value. This value is used when a component does not have a matching Provider.

The Provider allows the React components to subscribe to the context changes. These context changes can help us determine a user's logged in state in the chat app.

In this section, we will modify the App.js file to two stack navigators for Chat and Auth related screens. Let's start by adding the import statements and then defining a ChatStack and an AuthStack navigator functions.

import React, { useState, createContext, useContext, useEffect } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { View, ActivityIndicator } from 'react-native';
import { onAuthStateChanged } from 'firebase/auth';
import { auth } from './config/firebase';

import Login from './screens/Login';
import Signup from './screens/Signup';
import Chat from './screens/Chat';

const Stack = createStackNavigator();

function ChatStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name='Chat' component={Chat} />
    </Stack.Navigator>
  );
}

function AuthStack() {
  return (
    <Stack.Navigator screenOptions={{ headerShown: false }}>
      <Stack.Screen name='Login' component={Login} />
      <Stack.Screen name='Signup' component={Signup} />
    </Stack.Navigator>
  );
}
Enter fullscreen mode Exit fullscreen mode

To create an auth provider, export a function called AuthenticatedUserProvider. This provider is going to allow the screen components to access the current user in the application. Define a state variable called user.

Add the following code snippet:

const AuthenticatedUserContext = createContext({});

const AuthenticatedUserProvider = ({ children }) => {
  const [user, setUser] = useState(null);

  return (
    <AuthenticatedUserContext.Provider value={{ user, setUser }}>
      {children}
    </AuthenticatedUserContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Next, modify the RootNavigator function. Inside this function, we will use the Firebase method onAuthStateChanged() that is going to handle the user's logged-in state changes. Using the useEffect hook, you can subscribe to this state change function and make sure you unsubscribe it when the component unmounts. This method allows you to subscribe to real-time events when the user performs an action. The action here can be, logging in, signing out, and so on.

function RootNavigator() {
  const { user, setUser } = useContext(AuthenticatedUserContext);
  const [isLoading, setIsLoading] = useState(true);

   useEffect(() => {
    // onAuthStateChanged returns an unsubscriber
    const unsubscribeAuth = onAuthStateChanged(
      auth,
      async authenticatedUser => {
        authenticatedUser ? setUser(authenticatedUser) : setUser(null);
        setIsLoading(false);
      }
    );

    // unsubscribe auth listener on unmount
    return unsubscribeAuth;
  }, [user]);


  if (isLoading) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator size='large' />
      </View>
    );
  }

  return (
    <NavigationContainer>
      {user ? <ChatStack /> : <AuthStack />}
    </NavigationContainer>
  );
}
Enter fullscreen mode Exit fullscreen mode

Lastly, wrap the RootNavigator with AuthenticatedUserProvider inside App function:

export default function App() {
  return (
    <AuthenticatedUserProvider>
      <RootNavigator />
    </AuthenticatedUserProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Firebase authentication is implemented in our app:

firebase authentication implemented

Adding Chat Functionality

As the authentication in our chat application is now working, we can move ahead and add the chat functionality itself. This component is going to need the user information from Firebase in order to create a chat message and send it.

Start by importing the necessary component from the React Native Gifted Chat library, and auth and database object from the firebase config file and other methods from firebase/firestore to fetch and add data to the collection.

import React, {
  useState,
  useEffect,
  useLayoutEffect,
  useCallback
} from 'react';
import { TouchableOpacity, Text } from 'react-native';
import { GiftedChat } from 'react-native-gifted-chat';
import {
  collection,
  addDoc,
  orderBy,
  query,
  onSnapshot
} from 'firebase/firestore';
import { signOut } from 'firebase/auth';

import { auth, database } from '../config/firebase';
Enter fullscreen mode Exit fullscreen mode

Inside the Chat function, create a messages state and a function to handle logout action using useLayoutEffect as well as the business logic to log out a user inside the onSignOut handler method.

export default function Chat({ navigation }) {
  const [messages, setMessages] = useState([]);

const onSignOut = () => {
    signOut(auth).catch(error => console.log('Error logging out: ', error));
  };

  useLayoutEffect(() => {
    navigation.setOptions({
      headerRight: () => (
        <TouchableOpacity
          style={{
            marginRight: 10
          }}
          onPress={onSignOut}
        >
          <Text>Logout</Text>
        </TouchableOpacity>
      )
    });
  }, [navigation]);
Enter fullscreen mode Exit fullscreen mode

To retrieve old messages from the Firestore database, an API call has to be made to the database collection. We will set the collection name to chats and use the useLayoutEffect hook to make this database call.

To send a message, we will create a custom handler method called onSend. This method will use the useCallback hook and will store the messages in the Firestore collection called chats. It uses the addDoc method from Firestore to create a new document with an auto-generated id when a new message is sent.

useLayoutEffect(() => {
    const collectionRef = collection(database, 'chats');
    const q = query(collectionRef, orderBy('createdAt', 'desc'));

    const unsubscribe = onSnapshot(q, querySnapshot => {
      setMessages(
        querySnapshot.docs.map(doc => ({
          _id: doc.data()._id,
          createdAt: doc.data().createdAt.toDate(),
          text: doc.data().text,
          user: doc.data().user
        }))
      );
    });

return unsubscribe;
  });


const onSend = useCallback((messages = []) => {
    setMessages(previousMessages =>
      GiftedChat.append(previousMessages, messages)
    );
    const { _id, createdAt, text, user } = messages[0];    
    addDoc(collection(database, 'chats'), {
      _id,
      createdAt,
      text,
      user
    });
  }, []);

Enter fullscreen mode Exit fullscreen mode

Lastly, we will use the GiftedChat component and its different props. The first prop is messages to display messages. The next prop showAvatarForEveryMessage is set to true. We will set a random avatar for each user who logs in and sends a message for this example. You can replace it with your own logic to add a better avatar-generating solution.

The onSend prop is responsible for sending messages. The user object is to identify which user is sending the message.

return (
  <GiftedChat
    messages={messages}
    showAvatarForEveryMessage={true}
    onSend={messages => onSend(messages)}
    user={{
      _id: auth?.currentUser?.email,
      avatar: 'https://i.pravatar.cc/300'
    }}
  />
);
Enter fullscreen mode Exit fullscreen mode

Here is the output after this step:

chat functionality

Conclusion

Firebase is a great service in terms of time savings and faster app development. Integrating it with specific use cases (such as demonstrated in this tutorial) without building a complete backend from scratch is an advantage for any React Native developer.

Lastly, if you're building React Native applications with sensitive logic, be sure to protect them against code theft and reverse-engineering with Jscrambler.

Originally published on the Jscrambler Blog by Aman Mittal.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .