After this exercise, you will be able to:
- Implement protected routes that require user authentication.
- Conditionally render UI elements based on a user's login status.
- Structure data-fetching logic into custom React hooks.
- Secure database access using Firebase Security Rules.
- Build a full CRUD (Create, Read, Update, Delete) interface for authenticated users.
- Fork this repo.
- Clone this repo.
- You will need a Google account to create a Firebase project.
- Upon completion, run the following commands:
$ git add .
$ git commit -m "Solved lab"
$ git push origin master
- Create a Pull Request so that your TAs can check your work.
This LAB is not equipped with automated unit tests. You will test your application's functionality by running it in the browser.
- Run your React application using
npm run dev
. - Open the application in your browser (usually
http://localhost:5173
). - Use the browser's Developer Tools to check for console errors, inspect network requests, and debug your components.
The goal of this exercise is to build a simple "Community Wall" application where users can sign in with Google to post, edit, and delete their own messages. This will put the concepts from the lesson into practice, focusing on protected actions and data ownership.
This exercise is split into multiple iterations. You will start from scratch, building the project structure, connecting to Firebase, and then implementing the features step-by-step.
Before writing any code, you need to set up your Firebase project and your local React development environment.
-
Firebase Project:
- Go to the Firebase Console and create a new project.
- Enable Authentication: In the console, go to Build > Authentication and enable the Google sign-in provider.
- Enable Realtime Database: Go to Build > Realtime Database, create a database, and start it in "locked mode".
- Get Config Keys: In your Project Settings, register a new Web App (
</>
) and copy thefirebaseConfig
object.
-
React Project:
- Create a new React project using Vite:
npm create vite@latest community-wall -- --template react
- Navigate into the new directory:
cd community-wall
- Install the necessary dependencies:
npm install firebase react-firebase-hooks react-router-dom
- Create a
.env.local
file in the root of yourcommunity-wall
project. Paste yourfirebaseConfig
keys into it, prefixed withVITE_
:# .env.local VITE_API_KEY="your-api-key" VITE_AUTH_DOMAIN="your-auth-domain" VITE_DATABASE_URL="your-database-url" # ... and so on for all keys
- Create a new React project using Vite:
-
Folder Structure:
- Inside the
src
folder, create the following directories:components
,config
,hooks
, andpages
.
- Inside the
First, let's connect our app to Firebase and create a simple component to handle logging in and out.
-
Create the Firebase config file in
src/config/firebase.js
. This file will initialize Firebase and export the services we need.// src/config/firebase.js import { initializeApp } from 'firebase/app'; import { getAuth, GoogleAuthProvider, signInWithPopup, signOut } from 'firebase/auth'; import { getDatabase } from 'firebase/database'; const firebaseConfig = { apiKey: import.meta.env.VITE_API_KEY, authDomain: import.meta.env.VITE_AUTH_DOMAIN, databaseURL: import.meta.env.VITE_DATABASE_URL, projectId: import.meta.env.VITE_PROJECT_ID, storageBucket: import.meta.env.VITE_STORAGE_BUCKET, messagingSenderId: import.meta.env.VITE_MESSAGING_SENDER_ID, appId: import.meta.env.VITE_APP_ID }; const app = initializeApp(firebaseConfig); export const auth = getAuth(app); export const db = getDatabase(app); const googleProvider = new GoogleAuthProvider(); export const signInWithGoogle = async () => { try { await signInWithPopup(auth, googleProvider); } catch (err) { console.error(err); } }; export const signOutUser = async () => { try { await signOut(auth); } catch (err) { console.error(err); } };
-
Create an
Auth.jsx
component insrc/components/Auth.jsx
. This will display the user's status and provide login/logout buttons.[!TIP] Use the
useAuthState
hook fromreact-firebase-hooks/auth
. It's the cleanest way to get the current user, loading state, and any errors. -
Update
App.jsx
to display theAuth
component. For now, just put it in a simple layout.
⭐ **Click for Iteration 1 Solution** ⭐
src/components/Auth.jsx
import { useAuthState } from 'react-firebase-hooks/auth';
import { auth, signInWithGoogle, signOutUser } from '../config/firebase';
const Auth = () => {
const [user, loading] = useAuthState(auth);
if (loading) {
return <p>Loading...</p>;
}
return (
<nav>
{user ? (
<div>
<span>Welcome, {user.displayName}!</span>
<button onClick={signOutUser}>Sign Out</button>
</div>
) : (
<button onClick={signInWithGoogle}>Sign in with Google</button>
)}
</nav>
);
};
export default Auth;
src/App.jsx
import Auth from './components/Auth';
import './App.css';
function App() {
return (
<div className="App">
<header>
<h1>Community Wall</h1>
<Auth />
</header>
<main>
<p>Welcome to the community wall. Sign in to post a message!</p>
</main>
</div>
);
}
export default App;
Most apps have pages that should only be visible to logged-in users. Let's create a "Dashboard" page and protect it.
-
Create page components:
src/pages/HomePage.jsx
: A simple welcome page.src/pages/DashboardPage.jsx
: A placeholder page for authenticated users.src/pages/LoginPage.jsx
: A page that tells the user they need to log in. It can reuse theAuth
component.
-
Create a
ProtectedRoute.jsx
component insrc/components/ProtectedRoute.jsx
. This component is the key to protecting routes.- It should use the
useAuthState
hook. - If
loading
, it can show a loading message. - If there's no user, it should redirect to the
/login
page using theNavigate
component fromreact-router-dom
. - If there is a user, it should render its
children
.
- It should use the
-
Set up the router. Modify
App.jsx
(ormain.jsx
) to usereact-router-dom
to define your routes.- The
/
route should showHomePage
. - The
/login
route should showLoginPage
. - The
/dashboard
route should be wrapped by yourProtectedRoute
component.
- The
⭐ **Click for Iteration 2 Solution** ⭐
src/components/ProtectedRoute.jsx
import { useAuthState } from 'react-firebase-hooks/auth';
import { Navigate } from 'react-router-dom';
import { auth } from '../config/firebase';
const ProtectedRoute = ({ children }) => {
const [user, loading] = useAuthState(auth);
if (loading) {
return <div>Loading...</div>;
}
if (!user) {
return <Navigate to="/login" />;
}
return <>{children}</>;
};
export default ProtectedRoute;
src/pages/LoginPage.jsx
import Auth from '../components/Auth';
const LoginPage = () => {
return (
<div>
<h2>Login Required</h2>
<p>Please sign in to continue.</p>
<Auth />
</div>
);
};
export default LoginPage;
src/App.jsx
(Router Setup)
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import HomePage from './pages/HomePage';
import DashboardPage from './pages/DashboardPage';
import LoginPage from './pages/LoginPage';
import ProtectedRoute from './components/ProtectedRoute';
import Auth from './components/Auth';
import './App.css';
function App() {
return (
<Router>
<div className="App">
<header>
<h1>Community Wall</h1>
<nav>
<Link to="/">Home</Link> | <Link to="/dashboard">Dashboard</Link>
</nav>
<Auth />
</header>
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
</Routes>
</main>
</div>
</Router>
);
}
export default App;
Now for the main feature: posting messages to the wall. This involves creating, reading, and deleting data, and securing it so users can only delete their own posts.
-
Update Firebase Security Rules. Go to your Realtime Database rules in the Firebase Console and replace the default rules. The rules should allow anyone to read the posts, but only authenticated users to write. Deletion should be restricted to the post's original author.
{ "rules": { "posts": { ".read": "true", "$postId": { // User must be logged in to write (create/update/delete) ".write": "auth != null", // On CREATE: new data must have a `uid` matching the user's auth.uid ".validate": "newData.hasChildren(['text', 'uid']) && newData.child('uid').val() === auth.uid", // On DELETE: existing data's uid must match the user's auth.uid ".validate": "newData.val() === null ? data.child('uid').val() === auth.uid : true" } } } }
[!CAUTION] Security rules are critical. The rules above ensure data integrity. The first
.validate
ensures new posts are created correctly, and the second one checks ownership before allowing a delete (newData.val() === null
). -
Create a custom hook
usePosts.js
insrc/hooks/
. This hook will encapsulate all the logic for interacting with theposts
collection in your database. It should handle:- Fetching all posts in real-time.
- Adding a new post (tagged with the user's
uid
). - Deleting a post.
-
Create a
PostList.jsx
component insrc/components/
. This component will:- Use your
usePosts
hook to get the data and functions. - Display a form for adding a new post (only show this if the user is logged in).
- Map over the posts and display them.
- For each post, show a "Delete" button only if the current user is the author of the post.
- Use your
-
Integrate
PostList.jsx
into yourHomePage.jsx
orDashboardPage.jsx
so users can see and interact with the wall.
⭐ **Click for Iteration 3 Solution** ⭐
src/hooks/usePosts.js
import { useState, useEffect } from 'react';
import { useAuthState } from 'react-firebase-hooks/auth';
import { ref, onValue, push, remove, set } from 'firebase/database';
import { auth, db } from '../config/firebase';
export const usePosts = () => {
const [user] = useAuthState(auth);
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const postsRef = ref(db, 'posts');
const unsubscribe = onValue(postsRef, snapshot => {
const data = snapshot.val();
const postList = [];
if (data) {
for (const id in data) {
postList.push({ id, ...data[id] });
}
}
setPosts(postList.reverse()); // Show newest first
setLoading(false);
});
return () => unsubscribe();
}, []);
const addPost = async (text) => {
if (!user) throw new Error('Not authenticated');
try {
const postsRef = ref(db, 'posts');
const newPostRef = push(postsRef);
await set(newPostRef, {
text,
uid: user.uid,
author: user.displayName
});
} catch (error) {
console.error('Error adding post:', error);
}
};
const deletePost = async (postId) => {
if (!user) throw new Error('Not authenticated');
try {
const postRef = ref(db, `posts/${postId}`);
await remove(postRef);
} catch (error) {
console.error('Error deleting post:', error);
}
};
return { posts, loading, addPost, deletePost, user };
};
src/components/PostList.jsx
import { useState } from 'react';
import { usePosts } from '../hooks/usePosts';
const PostList = () => {
const { posts, loading, addPost, deletePost, user } = usePosts();
const [newPostText, setNewPostText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (newPostText.trim()) {
addPost(newPostText);
setNewPostText('');
}
};
if (loading) return <p>Loading posts...</p>;
return (
<section>
{user && (
<form onSubmit={handleSubmit}>
<h3>Create a new post</h3>
<textarea value={newPostText} onChange={e => setNewPostText(e.target.value)} placeholder="What's on your mind?" rows={3} />
<button type="submit">Post</button>
</form>
)}
<h2>Community Posts</h2>
<div className="post-container">
{posts.length === 0 ? (
<p>No posts yet. Be the first!</p>
) : (
posts.map(post => (
<div key={post.id} className="post-card">
<p>
<strong>{post.author || 'Anonymous'}</strong> wrote:
</p>
<p>{post.text}</p>
{user && user.uid === post.uid && <button onClick={() => deletePost(post.id)}>Delete</button>}
</div>
))
)}
</div>
</section>
);
};
export default PostList;
src/pages/HomePage.jsx
(Updated)
import PostList from '../components/PostList';
const HomePage = () => {
return (
<div>
<h2>Welcome!</h2>
<p>This is the community wall. See what others are saying and sign in to join the conversation.</p>
<hr />
<PostList />
</div>
);
};
export default HomePage;
Happy coding! ❤️