fervor [>]CODING & CURIOSITY
FERVOR LEARNING SYSTEMTUTORIALS
← Projects

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

  1. Go to the Firebase Console
  2. Click “Add project” and follow the setup wizard
  3. Enable Authentication (Email/Password to start)
  4. 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:

  1. User Roles & Permissions

    • Add a “role” field to user profiles
    • Implement different views for clients, team members, and vendors
  2. Group Chats & Direct Messages

    • Create rooms collection with different types
    • Add user selection for direct messages
  3. Rich Media Support

    • Integrate Firebase Storage for file uploads
    • Add image and file previews in messages
  4. Notifications

    • Implement Firebase Cloud Messaging for push notifications
    • Add email notifications for offline users
  5. 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.

Keep your curiosity going.Explore more Projects →
287 TUTORIALS · 22 TOPICSREADY