Web Development

Build a Chat App with Firebase and React Native

November 26th, 2021 | By Aman Mittal | 13 min read

Learn how to build a mobile chat app with Firebase, React Native, and Expo. 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 that works in real-time.

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

Installing Dependencies

You must have Expo CLI installed on your local machine and run the following commands from your terminal 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


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 ensure an Android Virtual Device is running before executing the command below.

# for iOS simalulor
yarn ios

# for Android device/emulator
yarn android


Next, install a dependency called react-native-gifted-chat that provides a customizable UI for a chat application.

For navigating between different screens, we use react-navigation. To connect with the Firebase project, we need the 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

To build the application, we are going to need:

  • A user authentication service.

  • A service to store the user's email.

  • A service to keep messages.


All of these services are going to be leveraged by Firebase. When building an authentication flow, we w won't go into the depths of implementing Firebase Auth with Expo. We have already covered that in-depth in a separate tutorial.

Setting up Firebase

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

Firebase can be integrated with a cloud service, the Google Cloud Platform.

We will 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 Add Project button.

Add the name of the new Firebase project and then click Continue.

When asked for the Google Analytics setup, you can disable it. Then, click Create Project. The home screen welcomes you.

Take a look at the side menu bar on the left. This is the main menu for 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 or password, and then hit the Save button.
setting-up-firebase-project

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

Click the Web button. It will prompt you to enter the details of your app. Enter the App nickname, and then click the Register app button.
register-the-new-app-by-enter-the-details-of-the-app-click-register-button

Then, Firebase will provide configuration objects with API keys and other keys required to use different Firebase services.
configuration-objects-API-keysThese 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 through Firebase security rules.

This does not mean that you should expose these keys to a version control host such as GitHub. We will follow the same methodology introduced in the blog article, How to Integrate Firebase Authentication with an Expo App. Here, we discussed how to set up environment variables in .env and use them using the expo-constants package.

Create a .env file at the root of your React Native project and 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


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 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
    }
  }
};


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();


Setup Firestore Database

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

Click Create Database. When asked for security rules, select test mode for this example.
create-database-option-select-test-mode

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.

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 />
  )
}


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 chat stack navigator containing only one screen. Later, we will add an AuthStack navigator with business logic to allow authenticated users to only view the 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 />;
}


Now if you run the simulator device, you will notice that there is a bare minimum chat screen that has a plain white header and 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-appears

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

Adding a Login Screen

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

The screen itself contains two input fields for the app user to enter their credentials and a button to log in to 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 were created using React Native.

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

The onHandleLogin method is going to authenticate a user's credentials using the 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 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
  }
});


Here is what the screen will look like:
adding-login-screen-example

Creating the Signup Screen

The Signup screen is similar to the log-in one. It has the same input fields and buttons, with only one exception.

The handler method defined in this file, called onHandleSignup, uses the 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
  }
});


Here is what the screen will look like:
firebase-screen-signup-option

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 have two stack navigators for Chat and authentication-related screens. Let's start by adding the import statements and then defining ChatStack and 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>
  );
}


To create an authentication 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>
  );
};


Next, modify the RootNavigator function. Inside this function, we will use the Firebase method onAuthStateChanged(), which 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 acts. 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>
  );
}


Lastly, wrap the RootNavigator with AuthenticatedUserProvider inside the App function:

export default function App() {
  return (
    <AuthenticatedUserProvider>
      <RootNavigator />
    </AuthenticatedUserProvider>
  );
}


Firebase authentication is implemented in our app.

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 to create a chat message and send it.

Start by importing the necessary components from the React Native Gifted Chat library, the auth and database objects 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';


Inside the Chat function, create a message state and a function to handle the 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]);


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 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.

useEffect(() => {
    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
    });
  }, []);


Lastly, we will use the GiftedChat component and its different props. The first prop is a message 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. You can replace it with your 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'
    }}
  />
);


Conclusion

Firebase is a great service for time savings and faster app development.

Integrating it with specific use cases (such as those demonstrated in this tutorial) without building a complete backend from scratch is an advantage for any React Native developer.

Jscrambler

The leader in client-side Web security. With Jscrambler, JavaScript applications become self-defensive and capable of detecting and blocking client-side attacks like Magecart.

View All Articles

Must read next

Cybersecurity

Can ChatGPT reverse engineer Jscrambler obfuscation?

As the potential of ChatGPT (and of Generative AI in general) is unveiled, experts and developers keep asking questions and experimenting with the tool. Can it crack even the strongest protections...

June 13, 2023 | By Jscrambler | 6 min read

Web Development

Build a Chatbot with Dialogflow and React Native

Learn how to build your first chatbot with Dialogflow and React Native and improve the user experience in your next mobile app.

March 26, 2019 | By Aman Mittal | 9 min read

Section Divider

Subscribe to Our Newsletter