DEV Community

César Fabián CHÁVEZ LINARES
César Fabián CHÁVEZ LINARES

Posted on

Building a Real-time Chat Application with Google Cloud Firestore and React

In this tutorial, we'll create a real-time chat application using Google Cloud Firestore and React. Firestore offers powerful real-time synchronization capabilities, making it perfect for chat applications, collaborative tools, and live dashboards.

Why Google Cloud Firestore?

While DynamoDB and CosmosDB are popular choices, Firestore offers unique advantages:

  • Real-time listeners with automatic data synchronization
  • Powerful querying capabilities
  • Offline data persistence
  • Automatic scaling
  • Simple SDK integration
  • Free tier for development and small applications

Prerequisites

  • Node.js and npm installed
  • Google Cloud account
  • Basic knowledge of React and JavaScript
  • Familiarity with async/await

Project Setup

First, create a new React project and install dependencies:

npx create-react-app chat-app cd chat-app npm install firebase @mui/material @mui/icons-material @emotion/react @emotion/styled date-fns 
Enter fullscreen mode Exit fullscreen mode

Firebase Configuration

  1. Create a new project in the Firebase Console
  2. Enable Firestore in your project
  3. Create src/firebase.js:
import { initializeApp } from 'firebase/app'; import { getFirestore } from 'firebase/firestore'; import { getAuth } from 'firebase/auth'; const firebaseConfig = { apiKey: "your-api-key", authDomain: "your-project.firebaseapp.com", projectId: "your-project-id", storageBucket: "your-project.appspot.com", messagingSenderId: "your-sender-id", appId: "your-app-id" }; const app = initializeApp(firebaseConfig); export const db = getFirestore(app); export const auth = getAuth(app); 
Enter fullscreen mode Exit fullscreen mode

Database Schema

Create the following collections in Firestore:

rooms/ ├── roomId/  ├── name: string  ├── description: string  └── createdAt: timestamp  messages/ ├── messageId/  ├── roomId: string  ├── userId: string  ├── text: string  ├── createdAt: timestamp  └── userName: string 
Enter fullscreen mode Exit fullscreen mode

Components Implementation

Create the chat components:

ChatRoom Component (src/components/ChatRoom.js):

import React, { useEffect, useState, useRef } from 'react'; import { collection, query, orderBy, limit, onSnapshot, addDoc, serverTimestamp } from 'firebase/firestore'; import { db, auth } from '../firebase'; import { Box, TextField, Button, Paper, Typography, List, ListItem, ListItemText } from '@mui/material'; import { formatDistance } from 'date-fns'; const ChatRoom = ({ roomId }) => { const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(''); const messagesEndRef = useRef(null); useEffect(() => { // Query messages for this room const q = query( collection(db, 'messages'), orderBy('createdAt', 'asc'), limit(100) ); // Subscribe to real-time updates const unsubscribe = onSnapshot(q, (snapshot) => { const newMessages = []; snapshot.forEach((doc) => { newMessages.push({ id: doc.id, ...doc.data() }); }); setMessages(newMessages); scrollToBottom(); }); return () => unsubscribe(); }, [roomId]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; const handleSubmit = async (e) => { e.preventDefault(); if (!newMessage.trim()) return; try { await addDoc(collection(db, 'messages'), { text: newMessage, createdAt: serverTimestamp(), userId: auth.currentUser.uid, userName: auth.currentUser.displayName || 'Anonymous', roomId }); setNewMessage(''); } catch (error) { console.error('Error sending message:', error); } }; return ( <Box sx={{ height: '100vh', display: 'flex', flexDirection: 'column' }}> <Paper elevation={3} sx={{ flex: 1, overflow: 'auto', p: 2, mb: 2 }}> <List> {messages.map((message) => ( <ListItem key={message.id} sx={{ display: 'flex', flexDirection: 'column', alignItems: message.userId === auth.currentUser?.uid ? 'flex-end' : 'flex-start' }} > <Typography variant="caption" color="textSecondary"> {message.userName}  { message.createdAt && formatDistance(message.createdAt.toDate(), new Date(), { addSuffix: true }) } </Typography>  <Paper elevation={1} sx={{ p: 1, bgcolor: message.userId === auth.currentUser?.uid ? 'primary.light' : 'grey.100', maxWidth: '70%' }} > <Typography>{message.text}</Typography>  </Paper>  </ListItem>  ))} <div ref={messagesEndRef} />  </List>  </Paper>  <Box component="form" onSubmit={handleSubmit} sx={{ p: 2 }}> <TextField fullWidth value={newMessage} onChange={(e) => setNewMessage(e.target.value)} placeholder="Type a message..." variant="outlined" sx={{ mr: 1 }} InputProps={{ endAdornment: ( <Button type="submit" variant="contained" disabled={!newMessage.trim()}> Send </Button>  ) }} />  </Box>  </Box>  ); }; export default ChatRoom; 
Enter fullscreen mode Exit fullscreen mode

Room List Component (src/components/RoomList.js):

import React, { useEffect, useState } from 'react'; import { collection, query, orderBy, onSnapshot, addDoc, serverTimestamp } from 'firebase/firestore'; import { db } from '../firebase'; import { List, ListItem, ListItemText, Button, Dialog, DialogTitle, DialogContent, TextField, DialogActions } from '@mui/material'; const RoomList = ({ onRoomSelect }) => { const [rooms, setRooms] = useState([]); const [open, setOpen] = useState(false); const [newRoomName, setNewRoomName] = useState(''); const [newRoomDescription, setNewRoomDescription] = useState(''); useEffect(() => { const q = query(collection(db, 'rooms'), orderBy('createdAt', 'desc')); const unsubscribe = onSnapshot(q, (snapshot) => { const newRooms = []; snapshot.forEach((doc) => { newRooms.push({ id: doc.id, ...doc.data() }); }); setRooms(newRooms); }); return () => unsubscribe(); }, []); const handleCreateRoom = async () => { if (!newRoomName.trim()) return; try { await addDoc(collection(db, 'rooms'), { name: newRoomName, description: newRoomDescription, createdAt: serverTimestamp() }); setOpen(false); setNewRoomName(''); setNewRoomDescription(''); } catch (error) { console.error('Error creating room:', error); } }; return ( <> <List> {rooms.map((room) => ( <ListItem key={room.id} button onClick={() => onRoomSelect(room.id)} > <ListItemText primary={room.name} secondary={room.description} />  </ListItem>  ))} </List>  <Button variant="contained" onClick={() => setOpen(true)}> Create New Room </Button>  <Dialog open={open} onClose={() => setOpen(false)}> <DialogTitle>Create New Chat Room</DialogTitle>  <DialogContent> <TextField autoFocus margin="dense" label="Room Name" fullWidth value={newRoomName} onChange={(e) => setNewRoomName(e.target.value)} />  <TextField margin="dense" label="Description" fullWidth multiline rows={2} value={newRoomDescription} onChange={(e) => setNewRoomDescription(e.target.value)} />  </DialogContent>  <DialogActions> <Button onClick={() => setOpen(false)}>Cancel</Button>  <Button onClick={handleCreateRoom} variant="contained"> Create </Button>  </DialogActions>  </Dialog>  </>  ); }; export default RoomList; 
Enter fullscreen mode Exit fullscreen mode

Main App Component (src/App.js):

import React, { useState } from 'react'; import { Container, Grid, Paper } from '@mui/material'; import ChatRoom from './components/ChatRoom'; import RoomList from './components/RoomList'; function App() { const [selectedRoom, setSelectedRoom] = useState(null); return ( <Container maxWidth="lg" sx={{ height: '100vh', py: 2 }}> <Grid container spacing={2} sx={{ height: '100%' }}> <Grid item xs={3}> <Paper sx={{ height: '100%', p: 2 }}> <RoomList onRoomSelect={setSelectedRoom} />  </Paper>  </Grid>  <Grid item xs={9}> {selectedRoom ? ( <ChatRoom roomId={selectedRoom} />  ) : ( <Paper sx={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> Select a room to start chatting </Paper>  )} </Grid>  </Grid>  </Container>  ); } export default App; 
Enter fullscreen mode Exit fullscreen mode

Security Rules

Set up Firestore security rules:

rules_version = '2'; service cloud.firestore { match /databases/{database}/documents {  match /messages/{messageId} { allow read: if true; allow create: if request.auth != null; allow update, delete: if request.auth != null && request.auth.uid == resource.data.userId; } match /rooms/{roomId} { allow read: if true; allow create: if request.auth != null; allow update, delete: if request.auth != null; } } } 
Enter fullscreen mode Exit fullscreen mode

Running the Application

  1. Set up environment variables:
cp .env.example .env # Add your Firebase configuration 
Enter fullscreen mode Exit fullscreen mode
  1. Start the development server:
npm start 
Enter fullscreen mode Exit fullscreen mode

Key Features

  1. Real-time message synchronization
  2. Room creation and management
  3. Message history persistence
  4. User authentication integration
  5. Responsive Material-UI design
  6. Automatic message timestamps
  7. Scroll-to-bottom functionality

Performance Considerations

When working with Firestore:

  1. Query Optimization

    • Use compound queries for better performance
    • Implement pagination for large datasets
    • Cache frequently accessed data
  2. Real-time Listeners

    • Detach listeners when components unmount
    • Limit the number of concurrent listeners
    • Use appropriate query limits
  3. Offline Support

    • Enable offline persistence for better user experience
    • Handle offline/online state transitions
    • Implement retry logic for failed operations
  4. Cost Optimization

    • Monitor read/write operations
    • Use batch operations when possible
    • Implement appropriate caching strategies

Deployment

  1. Build the application:
npm run build 
Enter fullscreen mode Exit fullscreen mode
  1. Deploy to Firebase Hosting:
npm install -g firebase-tools firebase login firebase init firebase deploy 
Enter fullscreen mode Exit fullscreen mode

Conclusion

This implementation showcases how to build a real-time chat application using Google Cloud Firestore. The combination of Firestore's real-time capabilities and React's component model makes it easy to create responsive, scalable applications.

Future enhancements could include:

  • File attachments
  • User presence indicators
  • Message reactions
  • Rich text formatting
  • Voice messages

The complete source code is available on GitHub: [Link to your repository]

Resources

Top comments (0)