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
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
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
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.
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.
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.
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.
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.
Then, Firebase will provide configuration objects with API keys and other keys that are required to use different Firebase services.
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
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
}
}
};
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();
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. You can learn more about Security Rules with Firebase here and later on, update your rules accordingly.
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 />
)
}
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 />;
}
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.
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 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
}
});
Here is how the screen will look like:
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
}
});
Here is how the screen will look like:
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>
);
}
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>
);
};
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>
);
}
Lastly, wrap the RootNavigator
with AuthenticatedUserProvider
inside 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 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';
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]);
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
});
}, []);
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'
}}
/>
);
Here is the output after this step:
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.