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
Firebase Configuration
- Create a new project in the Firebase Console
- Enable Firestore in your project
- 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);
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
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;
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;
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;
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;
}
}
}
Running the Application
- Set up environment variables:
cp .env.example .env
# Add your Firebase configuration
- Start the development server:
npm start
Key Features
- Real-time message synchronization
- Room creation and management
- Message history persistence
- User authentication integration
- Responsive Material-UI design
- Automatic message timestamps
- Scroll-to-bottom functionality
Performance Considerations
When working with Firestore:
-
Query Optimization
- Use compound queries for better performance
- Implement pagination for large datasets
- Cache frequently accessed data
-
Real-time Listeners
- Detach listeners when components unmount
- Limit the number of concurrent listeners
- Use appropriate query limits
-
Offline Support
- Enable offline persistence for better user experience
- Handle offline/online state transitions
- Implement retry logic for failed operations
-
Cost Optimization
- Monitor read/write operations
- Use batch operations when possible
- Implement appropriate caching strategies
Deployment
- Build the application:
npm run build
- Deploy to Firebase Hosting:
npm install -g firebase-tools
firebase login
firebase init
firebase deploy
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]
Top comments (0)