Projects / 10 MIN READ
Fervor Social App Simpleified
Fervor Social App made simple, with a focus on the basics
From the original Fervor library. Examples may use older package versions.
Building a Multi-Tier Communication App with React and Firebase
This comprehensive tutorial will guide you through building a modern communication app using React and Firebase. We’ll cover setting up a project with the latest tools, implementing authentication, creating real-time messaging, and structuring your app for different user roles.
Prerequisites
- Node.js (v14 or newer) & npm installed
- A Firebase account (free tier works fine)
- Basic knowledge of React and modern JavaScript
1. Project Setup
Create a New React App
npx create-react-app fervor-social-app
cd fervor-social-app
Install Required Dependencies
npm install firebase react-router-dom @mui/material @mui/icons-material @emotion/react @emotion/styled
2. Firebase Configuration
Create a Firebase Project
- Go to the Firebase Console
- Click “Add project” and follow the setup wizard
- Enable Authentication (Email/Password to start)
- Create a Firestore Database in test mode
Set Up Firebase in Your React App
Create a file at src/firebase.js:
// src/firebase.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
// Replace with your own Firebase config
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT_ID.appspot.com",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
// Initialize Firebase services
export const auth = getAuth(app);
export const db = getFirestore(app);
export default app;
3. Project Structure
Let’s organize our app with a clean folder structure:
src/
├── components/
│ ├── auth/
│ │ ├── Login.jsx
│ │ ├── Register.jsx
│ │ └── AuthGuard.jsx
│ ├── chat/
│ │ ├── ChatRoom.jsx
│ │ ├── MessageList.jsx
│ │ ├── MessageInput.jsx
│ │ └── ChatHeader.jsx
│ └── layout/
│ ├── Navbar.jsx
│ └── Sidebar.jsx
├── contexts/
│ └── AuthContext.jsx
├── hooks/
│ ├── useAuth.js
│ └── useMessages.js
├── firebase.js
├── App.jsx
└── index.js
4. Authentication Context
Create an authentication context to manage user state throughout the app:
// src/contexts/AuthContext.jsx
import React, { createContext, useEffect, useState } from 'react';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged
} from 'firebase/auth';
import { auth } from '../firebase';
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const [loading, setLoading] = useState(true);
const signup = (email, password) => {
return createUserWithEmailAndPassword(auth, email, password);
};
const login = (email, password) => {
return signInWithEmailAndPassword(auth, email, password);
};
const logout = () => {
return signOut(auth);
};
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = {
currentUser,
signup,
login,
logout
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
};
Create a custom hook for easy auth access:
// src/hooks/useAuth.js
import { useContext } from 'react';
import { AuthContext } from '../contexts/AuthContext';
export const useAuth = () => {
return useContext(AuthContext);
};
5. Authentication Components
Login Component
// src/components/auth/Login.jsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../hooks/useAuth';
import {
Container,
Paper,
TextField,
Button,
Typography,
Box,
Alert
} from '@mui/material';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
try {
setError('');
setLoading(true);
await login(email, password);
navigate('/chat');
} catch (err) {
setError('Failed to sign in: ' + err.message);
} finally {
setLoading(false);
}
};
return (
<Container maxWidth="sm">
<Paper elevation={3} sx={{ p: 4, mt: 8 }}>
<Typography variant="h4" component="h1" gutterBottom align="center">
Log In
</Typography>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<Box component="form" onSubmit={handleSubmit}>
<TextField
label="Email"
type="email"
fullWidth
margin="normal"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<TextField
label="Password"
type="password"
fullWidth
margin="normal"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
sx={{ mt: 3 }}
disabled={loading}
>
Log In
</Button>
<Box sx={{ mt: 2, textAlign: 'center' }}>
<Typography variant="body2">
Don't have an account?
<Button
onClick={() => navigate('/register')}
color="primary"
>
Register
</Button>
</Typography>
</Box>
</Box>
</Paper>
</Container>
);
};
export default Login;
Register Component
// src/components/auth/Register.jsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../hooks/useAuth';
import {
Container,
Paper,
TextField,
Button,
Typography,
Box,
Alert
} from '@mui/material';
const Register = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { signup } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
if (password !== confirmPassword) {
return setError('Passwords do not match');
}
try {
setError('');
setLoading(true);
await signup(email, password);
navigate('/chat');
} catch (err) {
setError('Failed to create an account: ' + err.message);
} finally {
setLoading(false);
}
};
return (
<Container maxWidth="sm">
<Paper elevation={3} sx={{ p: 4, mt: 8 }}>
<Typography variant="h4" component="h1" gutterBottom align="center">
Register
</Typography>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
<Box component="form" onSubmit={handleSubmit}>
<TextField
label="Email"
type="email"
fullWidth
margin="normal"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<TextField
label="Password"
type="password"
fullWidth
margin="normal"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<TextField
label="Confirm Password"
type="password"
fullWidth
margin="normal"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
sx={{ mt: 3 }}
disabled={loading}
>
Register
</Button>
<Box sx={{ mt: 2, textAlign: 'center' }}>
<Typography variant="body2">
Already have an account?
<Button
onClick={() => navigate('/login')}
color="primary"
>
Log In
</Button>
</Typography>
</Box>
</Box>
</Paper>
</Container>
);
};
export default Register;
Auth Guard Component
// src/components/auth/AuthGuard.jsx
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../../hooks/useAuth';
const AuthGuard = ({ children }) => {
const { currentUser } = useAuth();
if (!currentUser) {
return <Navigate to="/login" />;
}
return children;
};
export default AuthGuard;
6. Chat Components
Create a Custom Hook for Messages
// src/hooks/useMessages.js
import { useState, useEffect } from 'react';
import {
collection,
query,
orderBy,
limit,
onSnapshot,
addDoc,
serverTimestamp
} from 'firebase/firestore';
import { db } from '../firebase';
import { useAuth } from './useAuth';
export const useMessages = (roomId = 'general') => {
const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);
const { currentUser } = useAuth();
useEffect(() => {
const messagesRef = collection(db, 'rooms', roomId, 'messages');
const messagesQuery = query(
messagesRef,
orderBy('createdAt', 'asc'),
limit(100)
);
const unsubscribe = onSnapshot(messagesQuery, (snapshot) => {
const messagesData = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
setMessages(messagesData);
setLoading(false);
});
return unsubscribe;
}, [roomId]);
const sendMessage = async (text) => {
if (!currentUser) return;
const messagesRef = collection(db, 'rooms', roomId, 'messages');
await addDoc(messagesRef, {
text,
createdAt: serverTimestamp(),
uid: currentUser.uid,
email: currentUser.email,
displayName: currentUser.displayName || 'Anonymous'
});
};
return { messages, loading, sendMessage };
};
ChatRoom Component
// src/components/chat/ChatRoom.jsx
import React from 'react';
import { Box, Paper } from '@mui/material';
import ChatHeader from './ChatHeader';
import MessageList from './MessageList';
import MessageInput from './MessageInput';
import { useMessages } from '../../hooks/useMessages';
const ChatRoom = ({ roomId = 'general' }) => {
const { messages, loading, sendMessage } = useMessages(roomId);
return (
<Paper
elevation={3}
sx={{
display: 'flex',
flexDirection: 'column',
height: 'calc(100vh - 100px)',
m: 2
}}
>
<ChatHeader roomId={roomId} />
<Box sx={{
flexGrow: 1,
overflow: 'auto',
p: 2,
backgroundColor: '#f5f5f5'
}}>
<MessageList messages={messages} loading={loading} />
</Box>
<Box sx={{ p: 2, borderTop: '1px solid #ddd' }}>
<MessageInput onSendMessage={sendMessage} />
</Box>
</Paper>
);
};
export default ChatRoom;
MessageList Component
// src/components/chat/MessageList.jsx
import React, { useRef, useEffect } from 'react';
import { Box, Typography, Avatar, CircularProgress } from '@mui/material';
import { useAuth } from '../../hooks/useAuth';
const MessageList = ({ messages, loading }) => {
const { currentUser } = useAuth();
const messagesEndRef = useRef(null);
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
);
}
if (messages.length === 0) {
return (
<Box sx={{ textAlign: 'center', p: 3 }}>
<Typography color="textSecondary">
No messages yet. Start the conversation!
</Typography>
</Box>
);
}
return (
<Box>
{messages.map((message) => {
const isCurrentUser = currentUser && message.uid === currentUser.uid;
return (
<Box
key={message.id}
sx={{
display: 'flex',
justifyContent: isCurrentUser ? 'flex-end' : 'flex-start',
mb: 2
}}
>
<Box
sx={{
display: 'flex',
flexDirection: isCurrentUser ? 'row-reverse' : 'row',
alignItems: 'flex-end',
maxWidth: '70%'
}}
>
<Avatar
sx={{
bgcolor: isCurrentUser ? 'primary.main' : 'secondary.main',
width: 32,
height: 32,
ml: isCurrentUser ? 1 : 0,
mr: isCurrentUser ? 0 : 1
}}
>
{message.displayName?.[0] || message.email?.[0] || '?'}
</Avatar>
<Box>
<Box
sx={{
backgroundColor: isCurrentUser ? 'primary.light' : 'grey.100',
color: isCurrentUser ? 'white' : 'text.primary',
borderRadius: 2,
px: 2,
py: 1,
wordBreak: 'break-word'
}}
>
<Typography variant="body1">{message.text}</Typography>
</Box>
<Typography
variant="caption"
color="textSecondary"
sx={{
display: 'block',
mt: 0.5,
textAlign: isCurrentUser ? 'right' : 'left'
}}
>
{message.displayName || message.email?.split('@')[0]}
{message.createdAt ?
` • ${new Date(message.createdAt.toDate()).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` :
' • Sending...'}
</Typography>
</Box>
</Box>
</Box>
);
})}
<div ref={messagesEndRef} />
</Box>
);
};
export default MessageList;
MessageInput Component
// src/components/chat/MessageInput.jsx
import React, { useState } from 'react';
import { Box, TextField, IconButton } from '@mui/material';
import SendIcon from '@mui/icons-material/Send';
const MessageInput = ({ onSendMessage }) => {
const [message, setMessage] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (message.trim()) {
onSendMessage(message);
setMessage('');
}
};
return (
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex' }}>
<TextField
fullWidth
placeholder="Type a message..."
value={message}
onChange={(e) => setMessage(e.target.value)}
variant="outlined"
size="small"
/>
<IconButton
type="submit"
color="primary"
sx={{ ml: 1 }}
disabled={!message.trim()}
>
<SendIcon />
</IconButton>
</Box>
);
};
export default MessageInput;
ChatHeader Component
// src/components/chat/ChatHeader.jsx
import React from 'react';
import { Box, Typography, Button } from '@mui/material';
import { useAuth } from '../../hooks/useAuth';
import { useNavigate } from 'react-router-dom';
const ChatHeader = ({ roomId }) => {
const { logout } = useAuth();
const navigate = useNavigate();
const handleLogout = async () => {
await logout();
navigate('/login');
};
return (
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
p: 2,
borderBottom: '1px solid #ddd'
}}
>
<Typography variant="h6">
{roomId.charAt(0).toUpperCase() + roomId.slice(1)} Room
</Typography>
<Button
variant="outlined"
size="small"
onClick={handleLogout}
>
Logout
</Button>
</Box>
);
};
export default ChatHeader;
7. App Configuration with Routes
// src/App.jsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { CssBaseline, ThemeProvider, createTheme } from '@mui/material';
import { AuthProvider } from './contexts/AuthContext';
import AuthGuard from './components/auth/AuthGuard';
import Login from './components/auth/Login';
import Register from './components/auth/Register';
import ChatRoom from './components/chat/ChatRoom';
// Create a custom theme
const theme = createTheme({
palette: {
primary: {
main: '#3f51b5'
},
secondary: {
main: '#f50057'
}
}
});
function App() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<AuthProvider>
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="/chat"
element={
<AuthGuard>
<ChatRoom />
</AuthGuard>
}
/>
<Route path="*" element={<Navigate to="/login" />} />
</Routes>
</Router>
</AuthProvider>
</ThemeProvider>
);
}
export default App;
8. Setting Up Firestore Database Rules
In your Firebase console, navigate to Firestore Database > Rules and update with these rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Make sure users can only read and write to their own documents
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Chat room rules
match /rooms/{roomId} {
allow read: if request.auth != null;
// Messages in chat rooms
match /messages/{messageId} {
allow read: if request.auth != null;
allow create: if request.auth != null
&& request.resource.data.uid == request.auth.uid;
allow update, delete: if request.auth != null
&& resource.data.uid == request.auth.uid;
}
}
}
}
9. Running Your Application
npm start
Visit http://localhost:3000 to see your app in action.
10. Future Enhancements
Now that you have a working foundation, you can extend it with:
-
User Roles & Permissions
- Add a “role” field to user profiles
- Implement different views for clients, team members, and vendors
-
Group Chats & Direct Messages
- Create rooms collection with different types
- Add user selection for direct messages
-
Rich Media Support
- Integrate Firebase Storage for file uploads
- Add image and file previews in messages
-
Notifications
- Implement Firebase Cloud Messaging for push notifications
- Add email notifications for offline users
-
User Profiles
- Allow users to update display names and profile pictures
- Add user status indicators (online, away, offline)
This tutorial provides a solid foundation for building a multi-tier communication app with React and Firebase. The modular architecture makes it easy to extend with additional features as your needs grow.