Building a chat app with Socket.io and React Native 🤯
Chats are all around us, from Whatsapp to Facebook and Instagram, almost every platform offers a chat in some kind of variation. In Today's digital world we have all gone mobile! Just before this article I wrote a friend a message on Whatsapp.
So I have decided to make another one for React Native.
Chats are fun. You send a message to a person or group, and they see it and reply. Simple yet complex.To develop a chat app you would need to be aware of new messages as soon as they arrive.
In this article, we will do something a little bit different than the previous React tutorial. We will create a sign-in screen where you can enter your name, create groups where people can join and show real-time messages between the group members.
There are two ways to get live information from your server about a new bid:
Use long-polling HTTP request, basically an HTTP request every 5 – 10 seconds to get information about a new bid.
Use an open-socket (Websockets) to get information directly from the server when a new bid arrives.
In this article I will talk about Websockets and specifically on the Node.js library – Socket.io
Socket.io is a popular JavaScript library that allows us to create real-time, bi-directional communication between software applications and a Node.js server.
Just a quick background about us. Novu is the first open-source notification infrastructure. We basically help to manage all the product notifications. It can be In-App (the bell icon like you have in the Dev Community – Websockets), Emails, SMSs and so on.
I would be super happy if you could give us a star! It will help me to make more articles every week 🚀 https://github.com/novuhq/novu
In this tutorial, we’ll build the chat application with Expo – an open-source framework that enables us to create native apps for IOS and Android by writing React and JavaScript code.
Expo saves us from the complex configurations required to create a native application with React Native CLI, making it the easiest and fastest way to build and publish React Native apps.
Ensure you have the Expo CLI, Node.js, and Git installed on your computer. Then, create the project folder and an Expo React Native app by running the code below.
mkdir chat-appcd chat-appexpo init app
Expo allows us to create native applications using the Managed or Bare Workflow. We’ll use the blank Managed Workflow in this tutorial because all the necessary configurations have been completed for us.
? Choose a template: \u203a - Use arrow-keys. Return to submit. ----- Managed workflow -----\u276f blank a minimal app as clean as an empty canvas blank (TypeScript) same as blank but with TypeScript configuration tabs (TypeScript) several example screens and tabs using react-navigation and TypeScript ----- Bare workflow ----- minimal bare and minimal, just the essentials to get you started
Install Socket.io Client API to the React Native app.
cd appexpo install socket.io-client
Create a socket.js within a utils folder and copy the code below into the file
mkdir utilstouch socket.js//\ud83d\udc47\ud83c\udffb Paste within socket.js fileimport { io } from "socket.io-client";const socket = io.connect("http://localhost:4000");export default socket;
The code snippet above creates a real-time connection to the server hosted at that URL. (We’ll create the server in the upcoming section).
Create a styles.js file within the utils folder and copy the code below into the file. It contains all the styling for the chat application.
Here, I will guide you through creating the Socket.io Node.js server for real-time communication with the React Native application.
Create a server folder within the project folder.
cd chat-appmkdir server
Navigate into the server folder and create a package.json file.
cd server & npm init -y
Install Express.js, CORS, Nodemon, and Socket.io Server API.
npm install express cors nodemon socket.io
Express.js is a fast, minimalist framework that provides several features for building web applications in Node.js. CORS is a Node.js package that allows communication between different domains.
Nodemon is a Node.js tool that automatically restarts the server after detecting file changes, and Socket.io allows us to configure a real-time connection on the server.
Create an index.js file – the entry point to the Node.js server.
touch index.js
Set up a simple Node.js server using Express.js. The code snippet below returns a JSON object when you visit the http://localhost:4000/api in your browser.
Import the HTTP and the CORS library to allow data transfer between the client and the server domains.
const express = require("express");const app = express();const PORT = 4000;app.use(express.urlencoded({ extended: true }));app.use(express.json());//\ud83d\udc47\ud83c\udffb New importsconst http = require("http").Server(app);const cors = require("cors");app.use(cors());app.get("/api", (req, res) => { res.json({ message: "Hello world", });});http.listen(PORT, () => { console.log(`Server listening on ${PORT}`);});
Next, add Socket.io to the project to create a real-time connection. Before the app.get() block, copy the code below:
//\ud83d\udc47\ud83c\udffb New imports.....const socketIO = require('socket.io')(http, { cors: { origin: "" }});//\ud83d\udc47\ud83c\udffb Add this before the app.get() blocksocketIO.on('connection', (socket) => { console.log(`\u26a1: ${socket.id} user just connected!`); socket.on('disconnect', () => { socket.disconnect() console.log('\ud83d\udd25: A user disconnected'); });});
From the code snippet above, the socket.io("connection") function establishes a connection with the React app, then creates a unique ID for each socket and logs the ID to the console whenever you refresh the app.
When you refresh or close the app, the socket fires the disconnect event showing that a user has disconnected from the socket.
Configure Nodemon by adding the start command to the list of scripts in the package.json file. The code snippet below starts the server using Nodemon.
//\ud83d\udc47\ud83c\udffb In server/package.json"scripts": { "test": "echo \\"Error: no test specified\\" && exit 1", "start": "nodemon index.js" },
You can now run the server with Nodemon by using the command below.
Here, we’ll create the user interface for the chat application to enable users to sign in, create chat rooms, and send messages. The app is divided into three screens – the Login screen, the Chat screen, and the Messaging screen.
First, let’s set up React Navigation.
Create a screens folder within the app folder, add the Login, Chat, and Messaging components and render a "Hello World" text within them.
mkdir screenstouch Login.js Chat.js Messaging.js
Copy the code below into the App.js file within the app folder.
import React from "react";//\ud83d\udc47\ud83c\udffb app screensimport Login from "./screens/Login";import Messaging from "./screens/Messaging";import Chat from "./screens/Chat";//\ud83d\udc47\ud83c\udffb React Navigation configurationsimport { NavigationContainer } from "@react-navigation/native";import { createNativeStackNavigator } from "@react-navigation/native-stack";const Stack = createNativeStackNavigator();export default function App() { return ( </Stack.Navigator> </NavigationContainer> );}
import React, { useState } from "react";import { Text, SafeAreaView, View, TextInput, Pressable, Alert,} from "react-native";//\ud83d\udc47\ud83c\udffb Import the app stylesimport { styles } from "../utils/styles";const Login = ({ navigation }) => { const [username, setUsername] = useState(""); //\ud83d\udc47\ud83c\udffb checks if the input field is empty const handleSignIn = () => { if (username.trim()) { //\ud83d\udc47\ud83c\udffb Logs the username to the console console.log({ username }); } else { Alert.alert("Username is required."); } }; return ( Sign in</Text> setUsername(value)} /> </View> Get Started</Text> </View> </Pressable> </View> </SafeAreaView> );};export default Login;
The code snippet accepts the username from the user and logs it on the console.
Let’s update the code and save the username using Async Storage, so users will not be required to sign in to the application every time they launch the app.
💡 *Async Storage is a React Native package used to store string data in native applications. It is similar to the local storage on the web and can be used to store tokens and various data in string format.*
Here, we’ll update the user interface for the Chat screen to display the available chat rooms, allow users to create one, and navigate to the Messaging screen when each room is selected.GIF
Copy the code below into the Chat.js file.
import React from "react";import { View, Text, Pressable, SafeAreaView, FlatList } from "react-native";import { Feather } from "@expo/vector-icons";import ChatComponent from "../component/ChatComponent";import { styles } from "../utils/styles";const Chat = () => { //\ud83d\udc47\ud83c\udffb Dummy list of rooms const rooms = [ { id: "1", name: "Novu Hangouts", messages: [ { id: "1a", text: "Hello guys, welcome!", time: "07:50", user: "Tomer", }, { id: "1b", text: "Hi Tomer, thank you! \ud83d\ude07", time: "08:50", user: "David", }, ], }, { id: "2", name: "Hacksquad Team 1", messages: [ { id: "2a", text: "Guys, who's awake? \ud83d\ude4f\ud83c\udffd", time: "12:50", user: "Team Leader", }, { id: "2b", text: "What's up? \ud83e\uddd1\ud83c\udffb\u200d\ud83d\udcbb", time: "03:50", user: "Victoria", }, ], }, ]; return ( Chats</Text> {/* \ud83d\udc47\ud83c\udffb Logs "ButtonPressed" to the console when the icon is clicked */} console.log("Button Pressed!")}> </Pressable> </View> </View> {rooms.length > 0 ? ( } keyExtractor={(item) => item.id} /> ) : ( No rooms created!</Text> Click the icon above to create a Chat room</Text> </View> )} </View> </SafeAreaView> );};export default Chat;
From the code snippet above:
I created a dummy list of rooms, then rendered them through a FlatList into the ChatComponent.(yet to be created)
Since the rooms can either be empty or populated, the conditional statement determines the component to display.
Next, create the ChatComponent within a component folder. It represents a preview of each chat name, time, the last message sent and redirects users to the Messaging component when clicked.
Copy the code below into the components/ChatComponent.js file.
import { View, Text, Pressable } from "react-native";import React, { useLayoutEffect, useState } from "react";import { Ionicons } from "@expo/vector-icons";import { useNavigation } from "@react-navigation/native";import { styles } from "../utils/styles";const ChatComponent = ({ item }) => { const navigation = useNavigation(); const [messages, setMessages] = useState({}); //\ud83d\udc47\ud83c\udffb Retrieves the last message in the array from the item prop useLayoutEffect(() => { setMessages(item.messages[item.messages.length - 1]); }, []); ///\ud83d\udc47\ud83c\udffb Navigates to the Messaging screen const handleNavigation = () => { navigation.navigate("Messaging", { id: item.id, name: item.name, }); }; return ( {item.name}</Text> {messages?.text ? messages.text : "Tap to start chatting"} </Text> </View> {messages?.time ? messages.time : "now"} </Text> </View> </View> </Pressable> );};export default ChatComponent;
Congratulations💃🏻! We can now display the list of rooms and redirect the user the Messaging screen.
Before we proceed, let’s create a custom Modal component that allows users to create a new group (room) when we press the header icon.
Create a Modal.js file within the components folder, import it into the Chat screen and toggle it whenever we press the header icon.
import React from "react";import { View, Text, Pressable, SafeAreaView, FlatList } from "react-native";import { Feather } from "@expo/vector-icons";import ChatComponent from "../component/ChatComponent";import { styles } from "../utils/styles";//\ud83d\udc47\ud83c\udffb The Modal componentimport Modal from "../component/Modal";const Chat = () => { const [visible, setVisible] = useState(false); //...other variables return ( Chats</Text> {/* Displays the Modal component when clicked */} setVisible(true)}> </Pressable> </View> </View> ...</View> {/* Pass setVisible as prop in order to toggle the display within the Modal component. */} {visible ? : ""} </SafeAreaView> );};export default Chat;
Copy the code below into the Modal.js file.
import { View, Text, TextInput, Pressable } from "react-native";import React, { useState } from "react";import { styles } from "../utils/styles";const Modal = ({ setVisible }) => { const [groupName, setGroupName] = useState(""); //\ud83d\udc47\ud83c\udffb Function that closes the Modal component const closeModal = () => setVisible(false); //\ud83d\udc47\ud83c\udffb Logs the group name to the console const handleCreateRoom = () => { console.log({ groupName }); closeModal(); }; return ( Enter your Group name</Text> setGroupName(value)} /> CREATE</Text> </Pressable> CANCEL</Text> </Pressable> </View> </View> );};export default Modal;
import React, { useLayoutEffect, useState } from "react";import { View, TextInput, Text, FlatList, Pressable } from "react-native";import AsyncStorage from "@react-native-async-storage/async-storage";import MessageComponent from "../component/MessageComponent";import { styles } from "../utils/styles";const Messaging = ({ route, navigation }) => { const [chatMessages, setChatMessages] = useState([ { id: "1", text: "Hello guys, welcome!", time: "07:50", user: "Tomer", }, { id: "2", text: "Hi Tomer, thank you! \ud83d\ude07", time: "08:50", user: "David", }, ]); const [message, setMessage] = useState(""); const [user, setUser] = useState(""); //\ud83d\udc47\ud83c\udffb Access the chatroom's name and id const { name, id } = route.params;//\ud83d\udc47\ud83c\udffb This function gets the username saved on AsyncStorage const getUsername = async () => { try { const value = await AsyncStorage.getItem("username"); if (value !== null) { setUser(value); } } catch (e) { console.error("Error while loading username!"); } }; //\ud83d\udc47\ud83c\udffb Sets the header title to the name chatroom's name useLayoutEffect(() => { navigation.setOptions({ title: name }); getUsername() }, []); /*\ud83d\udc47\ud83c\udffb This function gets the time the user sends a message, then logs the username, message, and the timestamp to the console. */ const handleNewMessage = () => { const hour = new Date().getHours() < 10 ? `0${new Date().getHours()}` : `${new Date().getHours()}`; const mins = new Date().getMinutes() < 10 ? `0${new Date().getMinutes()}` : `${new Date().getMinutes()}`; console.log({ message, user, timestamp: { hour, mins }, }); }; return ( {chatMessages[0] ? ( ( )} keyExtractor={(item) => item.id} /> ) : ( "" )} </View> setMessage(value)} /> SEND</Text> </View> </Pressable> </View> </View> );};export default Messaging;
The code snippet above renders the messages in each chatroom via the MessageComponent component.
Create the MessageComponent file and copy the code below into the file:
import { View, Text } from "react-native";import React from "react";import { Ionicons } from "@expo/vector-icons";import { styles } from "../utils/styles";export default function MessageComponent({ item, user }) { const status = item.user !== user; return ( {item.text}</Text> </View> </View> {item.time}</Text> </View> </View> );}
From the code snippet above, the status variable checks if the user key on the message is the same as the current user so as to determine how to align the messages.
We’ve now completed the user interface for the application!🎊 Next, let’s learn how to communicate with the Socket.io server, create chat rooms, and send messages in real time via Socket.io.
In the previous section, we were able to create new chat rooms, save them in an array on the server, and display them within the app. Here, we’ll update the chat room messages by adding new messages to the sub-array.
Recall that the id of each chat room is passed into the Messaging component. Now, let’s send the id to the server via Socket.io when the screen loads.
import React, { useEffect, useLayoutEffect, useState } from "react";import { View, TextInput, Text, FlatList, Pressable } from "react-native";import socket from "../utils/socket";import MessageComponent from "../component/MessageComponent";import { styles } from "../utils/styles";const Messaging = ({ route, navigation }) => { //\ud83d\udc47\ud83c\udffb The id passed const { name, id } = route.params;//...other functions useLayoutEffect(() => { navigation.setOptions({ title: name }); //\ud83d\udc47\ud83c\udffb Sends the id to the server to fetch all its messages socket.emit("findRoom", id); }, []); return ...</View>;};export default Messaging;
Create the event listener on the server.
socket.on("findRoom", (id) => { //\ud83d\udc47\ud83c\udffb Filters the array by the ID let result = chatRooms.filter((room) => room.id == id); //\ud83d\udc47\ud83c\udffb Sends the messages to the app socket.emit("foundRoom", result[0].messages);});
Next, listen to the foundRoom event and display the messages to the user.
//\ud83d\udc47\ud83c\udffb This runs only initial mountuseLayoutEffect(() => { navigation.setOptions({ title: name }); socket.emit("findRoom", id); socket.on("foundRoom", (roomChats) => setChatMessages(roomChats));}, []);//\ud83d\udc47\ud83c\udffb This runs when the messages are updated.useEffect(() => { socket.on("foundRoom", (roomChats) => setChatMessages(roomChats));}, [socket])
Listen to the event on the server and update the chatRoom array.
socket.on("newMessage", (data) => { //\ud83d\udc47\ud83c\udffb Destructures the property from the object const { room_id, message, user, timestamp } = data; //\ud83d\udc47\ud83c\udffb Finds the room where the message was sent let result = chatRooms.filter((room) => room.id == room_id); //\ud83d\udc47\ud83c\udffb Create the data structure for the message const newMessage = { id: generateID(), text: message, user, time: `${timestamp.hour}:${timestamp.mins}`, }; //\ud83d\udc47\ud83c\udffb Updates the chatroom messages socket.to(result[0].name).emit("roomMessage", newMessage); result[0].messages.push(newMessage); //\ud83d\udc47\ud83c\udffb Trigger the events to reflect the new changes socket.emit("roomsList", chatRooms); socket.emit("foundRoom", result[0].messages);});
So far, you’ve learnt how to set up Socket.io in a React Native and Node.js application, save data with Async Storage and communicate between a server and the Expo app via Socket.io.
Socket.io is a great tool with excellent features that enables us to build efficient real-time applications like sports betting websites, auction and forex trading applications, and of course, chat applications by creating lasting connections with a Node.js server.
P.S Novu is sending awesome swag on Hacktoberfest! Come and participate! Happy if you can support us by giving us a star! ⭐️ https://github.com/novuhq/novu