This article covers how to make a User to User private Chat App using React JS and Firebase (without socket programming). React is a JavaScript framework provided by Facebook and is used to build fully-featured web applications.
We will be following the below steps for creating our application:
npx create-react-app chat-app
cd chat-app
Now install all required modules for the project by using the below command:
npm install @emotion/react @emotion/styled @mui/icons-material @mui/lab @mui/material firebase react-router-dom
Step 1: Open the “src” folder and select the App.js file.
This file contains routing logic, where routing helps the user to navigate different pages. Go through react-router-dom documentation to understand more about routing in React JS
App.js: Below is the code for the App.js file:
Javascript
import "./App.css" ; import { BrowserRouter, Routes, Route } from "react-router-dom" ; import SignIn from "./Screens/Signin" ; import SignUp from "./Screens/Signup" ; import ChatHome from "./Screens/ChatHome" ; function App() { return ( <div className= "App" > <BrowserRouter> <Routes> <Route exact path= "/" element={<SignIn />} /> <Route path= "/Signup" element={<SignUp />} /> <Route path= "/chat-home/:receiverId" element={<ChatHome />} /> </Routes> </BrowserRouter> </div> ); } export default App; |
Step 2: Create the “Screens” folder and Create files “Signin.js”, “Signup.js“, “ChatHome.js”
Step 3: Working with the Signin.js file
Here users need to enter their email and password, Firebase will authenticate
- If the user is not registered give an alert as the user has not found
- If the user entered the wrong credentials gives an alert as the wrong password
After Sign in success, users navigate to the Chat home page, where chatting takes place.
Signin.js: Below is the code for the Signin.js file.
Signin.js
import * as React from "react" ; import Avatar from "@mui/material/Avatar" ; import Button from "@mui/material/Button" ; import CssBaseline from "@mui/material/CssBaseline" ; import TextField from "@mui/material/TextField" ; import FormControlLabel from "@mui/material/FormControlLabel" ; import Checkbox from "@mui/material/Checkbox" ; import Link from "@mui/material/Link" ; import Grid from "@mui/material/Grid" ; import Box from "@mui/material/Box" ; import LockOutlinedIcon from "@mui/icons-material/LockOutlined" ; import Typography from "@mui/material/Typography" ; import Container from "@mui/material/Container" ; import { createTheme, ThemeProvider } from "@mui/material/styles" ; import { useNavigate } from "react-router-dom" ; import { signInWithEmailAndPassword } from "firebase/auth" ; import { auth } from "../Firebase" ; const theme = createTheme(); export default function SignIn() { const [email, setEmail] = React.useState( "" ); const [password, setPassword] = React.useState( "" ); const navigate = useNavigate(); const handleSubmit = async (event) => { event.preventDefault(); signInWithEmailAndPassword(auth, email, password) .then((userCredential) => { // Signed in const user = userCredential.user; navigate( "/chat-home/1" ); // ... }) . catch ((error) => { const errorCode = error.code; const errorMessage = error.message; alert(errorMessage); }); }; return ( <ThemeProvider theme={theme}> <Container component= "main" maxWidth= "xs" > <CssBaseline /> <Box sx={{ marginTop: 8, display: "flex" , flexDirection: "column" , alignItems: "center" , }} > <Avatar sx={{ m: 1, bgcolor: "secondary.main" }}> <LockOutlinedIcon /> </Avatar> <Typography component= "h1" variant= "h5" > Sign in </Typography> <Box component= "form" onSubmit={handleSubmit} noValidate sx={{ mt: 1 }} > <TextField margin= "normal" required fullWidth id= "email" label= "Email Address" name= "email" autoComplete= "email" autoFocus value={email} onChange={(e) => setEmail(e.target.value)} /> <TextField margin= "normal" required fullWidth name= "password" label= "Password" type= "password" id= "password" autoComplete= "current-password" value={password} onChange={(e) => setPassword(e.target.value)} /> <FormControlLabel control={<Checkbox value= "remember" color= "primary" />} label= "Remember me" /> <Button type= "submit" fullWidth variant= "contained" sx={{ mt: 3, mb: 2 }} > Sign In </Button> <Grid container> <Grid item xs> <Link href= "#" variant= "body2" > Forgot password? </Link> </Grid> <Grid item> <Link href= "/Signup" variant= "body2" > { "Don't have an account? Sign Up" } </Link> </Grid> </Grid> </Box> </Box> </Container> </ThemeProvider> ); } |
Output:
Step 4: Working with the Signup.js file
Here users need to Register/Signup with a username, email, and password. After registration for every user, there will be a unique id is generated and User data is stored in Firestore.
After Registration, the user navigates to the Sign-in page.
Signup.js: Below is the code for the “Signup.js” file.
Javascript
import * as React from "react" ; import Avatar from "@mui/material/Avatar" ; import Button from "@mui/material/Button" ; import CssBaseline from "@mui/material/CssBaseline" ; import TextField from "@mui/material/TextField" ; import Link from "@mui/material/Link" ; import Grid from "@mui/material/Grid" ; import Box from "@mui/material/Box" ; import LockOutlinedIcon from "@mui/icons-material/LockOutlined" ; import Typography from "@mui/material/Typography" ; import Container from "@mui/material/Container" ; import { createTheme, ThemeProvider } from "@mui/material/styles" ; import { auth, db } from "../Firebase" ; import { createUserWithEmailAndPassword, updateProfile } from "firebase/auth" ; import { doc, setDoc } from "firebase/firestore" ; import { useNavigate } from "react-router-dom" ; function Copyright(props) { return ( <Typography variant= "body2" color= "text.secondary" align= "center" {...props} > { "Copyright © " } Your Website </Link>{ " " } { new Date().getFullYear()} { "." } </Typography> ); } const theme = createTheme(); export default function SignUp() { const [username, setUsername] = React.useState( "" ); const [email, setEmail] = React.useState( "" ); const [password, setPassword] = React.useState( "" ); const navigate = useNavigate(); const handleSubmit = async (event) => { event.preventDefault(); try { const userCredential = await createUserWithEmailAndPassword( auth, email, password ); const update = await updateProfile(auth.currentUser, { displayName: username, }); const user = userCredential.user; setDoc(doc(db, "users" , user.uid), { username: username, email: email, userId: user.uid, timestamp: new Date(), }); navigate( "/" ); } catch (error) { alert(error.message); } }; return ( <ThemeProvider theme={theme}> <Container component= "main" maxWidth= "xs" > <CssBaseline /> <Box sx={{ marginTop: 8, display: "flex" , flexDirection: "column" , alignItems: "center" , }} > <Avatar sx={{ m: 1, bgcolor: "secondary.main" }}> <LockOutlinedIcon /> </Avatar> <Typography component= "h1" variant= "h5" > Sign up </Typography> <Box component= "form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }} > <Grid container spacing={2}> <Grid item xs={12}> <TextField autoComplete= "given-name" name= "firstName" required fullWidth id= "firstName" label= "First Name" autoFocus value={username} onChange={(e) => setUsername(e.target.value)} /> </Grid> <Grid item xs={12}> <TextField required fullWidth id= "email" label= "Email Address" name= "email" autoComplete= "email" value={email} onChange={(e) => setEmail(e.target.value)} /> </Grid> <Grid item xs={12}> <TextField required fullWidth name= "password" label= "Password" type= "password" id= "password" autoComplete= "new-password" value={password} onChange={(e) => setPassword(e.target.value)} /> </Grid> </Grid> <Button type= "submit" fullWidth variant= "contained" sx={{ mt: 3, mb: 2 }} > Sign Up </Button> <Grid container justifyContent= "flex-end" > <Grid item> <Link href= "/" variant= "body2" > Already have an account? Sign in </Link> </Grid> </Grid> </Box> </Box> <Copyright sx={{ mt: 5 }} /> </Container> </ThemeProvider> ); } |
Step 5: Working with the ChatHome.js file
Here users find all registered users on the left side, by clicking on any target user navigate to the private chat room with the target user, and start sending messages.
Now where actual chatting logic comes, whenever the user clicks on send message button, the below sendMessage function triggers and
creates a firestorm reference as users->userid->chatUsers->receiverId->messages for sender user and receiver user.
A Reference represents a specific location in your Database and can be used for reading or writing data to that Database location
const sendMessage = async () => { try { if (user && receiverData) { await addDoc( collection( db, "users",// Collection user.uid,// sender doc id "chatUsers",//Collection receiverData.userId,//receiver doc id "messages"// Collection ), { username: user.displayName, messageUserId: user.uid, message: chatMessage, timestamp: new Date(), } ); await addDoc( collection( db, "users",//Collection receiverData.userId,// receiver doc id "chatUsers",//Collection user.uid,//sender doc id "messages"//Collection ), { username: user.displayName, messageUserId: user.uid, message: chatMessage, timestamp: new Date(), } ); } } catch (error) { console.log(error); } setChatMessage(""); };
below useEffect React hook is for reading all messages for the “messages” collection in Firebase, to know more about how react hooks works go through React hooks documentation
useEffect(() => { if (receiverData) { const unsub = onSnapshot( query( collection( db, "users", user?.uid, "chatUsers", receiverData?.userId, "messages" ), orderBy("timestamp") ), (snapshot) => { setAllMessages( snapshot.docs.map((doc) => ({ id: doc.id, messages: doc.data(), })) ); } ); return unsub; } }, [receiverData?.userId]);
ChatHome.js: Below is the code for the “ChatHome.js” file.
Javascript
import React, { useEffect, useState } from "react" ; import Paper from "@mui/material/Paper" ; import { Button, Divider, IconButton } from "@mui/material" ; import SendIcon from "@mui/icons-material/Send" ; import List from "@mui/material/List" ; import ListItem from "@mui/material/ListItem" ; import ListItemButton from "@mui/material/ListItemButton" ; import ListItemText from "@mui/material/ListItemText" ; import ListItemAvatar from "@mui/material/ListItemAvatar" ; import Avatar from "@mui/material/Avatar" ; import { useNavigate } from "react-router" ; import { db, auth } from "../Firebase" ; import { addDoc, collection, onSnapshot, orderBy, query, } from "firebase/firestore" ; function UsersComponent(props) { const handleToggle = (username, userId) => { props.setReceiverData({ username: username, userId: userId, }); props.navigate(`/chat-home/${userId}`); }; return ( <List dense sx={{ width: "100%" , maxWidth: 360, bgcolor: "background.paper" }} > {props.users?.map((value, index) => { const labelId = `checkbox-list-secondary-label-${value}`; if (props.currentUserId !== value.userId) return ( <ListItem key={value.userId} disablePadding> <ListItemButton onClick={() => { handleToggle(value.username, value.userId); }} > <ListItemAvatar> <Avatar alt={`${value.username}`} src={`${value.username}.jpg`} /> </ListItemAvatar> <ListItemText id={labelId} primary={`${value.username}`} /> </ListItemButton> </ListItem> ); })} </List> ); } export default function Home() { const [users, setUsers] = useState([]); const [receiverData, setReceiverData] = useState( null ); const [chatMessage, setChatMessage] = useState( "" ); const [allMessages, setAllMessages] = useState([]); const user = auth.currentUser; const navigate = useNavigate(); useEffect(() => { const unsub = onSnapshot(collection(db, "users" ), (snapshot) => { setUsers(snapshot.docs.map((doc) => doc.data())); }); return unsub; }, []); useEffect(() => { if (receiverData) { const unsub = onSnapshot( query( collection( db, "users" , user?.uid, "chatUsers" , receiverData?.userId, "messages" ), orderBy( "timestamp" ) ), (snapshot) => { setAllMessages( snapshot.docs.map((doc) => ({ id: doc.id, messages: doc.data(), })) ); } ); return unsub; } }, [receiverData?.userId]); const sendMessage = async () => { try { if (user && receiverData) { await addDoc( collection( db, "users" , user.uid, "chatUsers" , receiverData.userId, "messages" ), { username: user.displayName, messageUserId: user.uid, message: chatMessage, timestamp: new Date(), } ); await addDoc( collection( db, "users" , receiverData.userId, "chatUsers" , user.uid, "messages" ), { username: user.displayName, messageUserId: user.uid, message: chatMessage, timestamp: new Date(), } ); } } catch (error) { console.log(error); } setChatMessage( "" ); }; return ( <div style={root}> <Paper style={left}> <div style={{ display: "flex" , padding: 5, justifyContent: "space-between" , }} > <h4 style={{ margin: 0 }}>{user?.displayName} </h4> <Button color= "secondary" onClick={() => { auth.signOut(); navigate( "/" ); }} > Logout </Button> </div> <Divider /> All users <div style={{ overflowY: "scroll" }}> <UsersComponent users={users} setReceiverData={setReceiverData} navigate={navigate} currentUserId={user?.uid} /> </div> </Paper> <Paper style={right}> <h4 style={{ margin: 2, padding: 10 }}> {receiverData ? receiverData.username : user?.displayName}{ " " } </h4> <Divider /> <div style={messagesDiv}> { /* messages area */ } {allMessages && allMessages.map(({ id, messages }) => { return ( <div key={id} style={{ margin: 2, display: "flex" , flexDirection: user?.uid == messages.messageUserId ? "row-reverse" : "row" , }} > <span style={{ backgroundColor: "#BB8FCE" , padding: 6, borderTopLeftRadius: user?.uid == messages.messageUserId ? 10 : 0, borderTopRightRadius: user?.uid == messages.messageUserId ? 0 : 10, borderBottomLeftRadius: 10, borderBottomRightRadius: 10, maxWidth: 400, fontSize: 15, textAlign: user?.uid == messages.messageUserId ? "right" : "left" , }} > {messages.message} </span> </div> ); })} </div> <div style={{ width: "100%" , display: "flex" , flex: 0.08 }}> <input value={chatMessage} onChange={(e) => setChatMessage(e.target.value)} style={input} type= "text" placeholder= "Type message..." /> <IconButton onClick={sendMessage}> <SendIcon style={{ margin: 10 }} /> </IconButton> </div> </Paper> </div> ); } const root = { display: "flex" , flexDirection: "row" , flex: 1, width: "100%" , }; const left = { display: "flex" , flex: 0.2, height: "95vh" , margin: 10, flexDirection: "column" , }; const right = { display: "flex" , flex: 0.8, height: "95vh" , margin: 10, flexDirection: "column" , }; const input = { flex: 1, outline: "none" , borderRadius: 5, border: "none" , }; const messagesDiv = { backgroundColor: "#FBEEE6" , padding: 5, display: "flex" , flexDirection: "column" , flex: 1, maxHeight: 460, overflowY: "scroll" , }; |
Step 6: Open Firebase go to console -> Create a new project -> copy the firebaseConfig. Now Create “Firebase.js” file in src folder
Step 7: working with the Firebase.js file
Here we are going to integrate Firebase with React JS, Go through Firebase Documentation to find the Integration logic.
Now paste the firebaseConfig in the below-commented place
Firebase.js: The below code is for the “Firebase.js” file
Javascript
import { initializeApp } from "firebase/app" ; import { getFirestore } from "firebase/firestore" ; import { getAuth } from "firebase/auth" ; const firebaseConfig = { // paste Copied firebaseConfig here }; const app = initializeApp(firebaseConfig); const db = getFirestore(app); const auth = getAuth(app); export { db, auth }; |
Step 8: Running and Building the application: We can run this application by using the following command. This will start React’s development server that can be used for debugging our application.
npm run start
Output: