- Why Firebase?
- Why React?
- Setting up the Project
- Creating a New Firebase Project
- Creating a New App in the Firebase Project
- Adding a New Firebase Cloud Firestore
- Adding a New Collection to the Firebase Cloud Firestore
- Updating the Rules of the Firebase Cloud Firestore
- Integrating Firebase with Create React App
- Pushing Our Code Changes to GitHub
- Deploying the Application to Vercel
- Conclusion
- Frequently Asked Questions (FAQs) about Building a Reddit Clone with React and Firebase
React is a fantastic front-end library for building user interfaces. When picking a back end to use alongside it, you can’t go far wrong with Firebase, a Backend-as-a-Service (Baas) that makes it easy to add data persistence (and much more besides) to your React app.
In this tutorial, we’ll be using Firebase along with Create React App to build an application that will function similarly to Reddit. It will allow the user to submit a new post that can then be voted on. I’ll also demonstrate how to deploy our Reddit clone to Vercel.
Once you’ve finished reading, you’ll understand how to set up Firebase, how to connect it to your React app and how to deploy the result.
Why Firebase?
One of Firebase’s strengths is that it makes it very easy for us to show real-time data to the user. Once a user votes on a link, the feedback will be instantaneous. Firebase’s Realtime Database will help us in developing this feature. Also, it will help us to understand how to bootstrap a React application with Firebase.
Why React?
React is particularly known for creating user interfaces using a component architecture. Each component can contain internal state or be passed data as props. State and props are the two most important concepts in React. These two things help us determine the state of our application at any point in time. If you’re not familiar with these terms, please head over to the React docs first.
Note: you can also use a state container like Redux or MobX, but for the sake of simplicity, we won’t be using one for this tutorial.
Here’s a live demo of what we’ll be building. The code for this application is available on GitHub.
Setting up the Project
To follow along, you’ll need to have Node and npm installed on your machine. If you haven’t, head to the Node.js download page and grab the latest version for your system (npm comes bundled with Node). Alternatively, you can consult our tutorial on installing Node using a version manager.
Let’s walk through the steps to set up our project structure and any necessary dependencies.
Bootstrapping a React App
We can create a new React application with the help of Create React App using the following command:
npx create-react-app reddit-clone
This will scaffold a new create-react-app
project inside the reddit-clone
directory. Our directory structure should be as follows:
Once the bootstrapping is done, we can enter the reddit-clone
directory and fire up the development server:
cd reddit-clone && npm start
At this point, we can visit http://localhost:3000/ and see our application up and running.
Structuring the App
It’s always a good practice to remove all the files that we don’t need after bootstrapping any application. There are a few files generated by Create React App that we won’t need, so we’ll remove them.
We can remove the following files:
src/App.css
src/App.test.js
src/index.css
src/logo.svg
src/serviceWorker.js
src/setupTests.js
We can also remove the following dependencies from our package.json
file:
@testing-library/jest-dom
@testing-library/react
@testing-library/user-event
We can also remove the test
script from our package.json
file. This is because we won’t be writing any tests for our application. If testing a React app is something you’d like to look into, please consult our tutorial, “How to Test React Components Using Jest”.
Our src/index.js
file should contain the following:
import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
We’ll rename our src/App.js
to src/app.js
. Alter it to contain the following:
import React from "react";
function App() {
return <div>Hello world!</div>;
}
export default App;
Now, we can restart our development server using the following command from our root directory:
npm start
Our development server should be up and running on http://localhost:3000/ and it should look like the following:
Creating a New Firebase Project
In this section, we’ll be installing and integrating Firebase with our application.
If you don’t have a Firebase account, you can create one free account now by visiting their website. After you’re done creating a new account, log in to your account and go to the console page and click on Create a project.
Enter the name of your project (I’ll call mine reddit-clone), accept the terms and conditions, and click on the Continue button.
In the next step, you should choose whether to enable Google Analytics for the project, then click on the Continue button.
In step three, we should select a Google Analytics account and then click on the Create project button:
After a short while, you’ll see a notice that your new project is ready. Click Continue to exit the wizard.
Creating a New App in the Firebase Project
In this section, we’ll be creating a new Firebase app from the Firebase console. We can create a Web app by selecting the web option.
Next, we’ll need to enter the name of the project and click on the Register app button, leaving the Also set up Firebase Hosting checkbox unchecked.
Now you’ll see all the credentials for our new Firebase web app.
Make a note of these credentials and click Continue to console.
We can now add our app’s credentials to an environment file:
// .env
REACT_APP_FIREBASE_API_KEY="123456"
REACT_APP_FIREBASE_AUTH_DOMAIN="reddit-clone-123456.firebaseapp.com"
REACT_APP_FIREBASE_PROJECT_ID="reddit-clone-123456"
REACT_APP_FIREBASE_STORAGE_BUCKET="reddit-clone-123456.appspot.com"
REACT_APP_FIREBASE_MESSAGING_SENDER_ID="123456"
REACT_APP_FIREBASE_APP_ID="1:123456:web:123456"
REACT_APP_FIREBASE_MEASUREMENT_ID="G-123456"
Note: it’s always a good idea to store all credentials in an environment file and add that file to .gitignore
so that the credentials are never leaked into the source code.
Next, we can create a new file src/lib/firebase.js
where we’ll store all our Firebase credentials:
import firebase from "firebase";
const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_FIREBASE_APP_ID,
measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID,
};
const initFirebase = firebase.initializeApp(firebaseConfig);
const db = initFirebase.firestore();
export default db;
Finally, we’ll need to install the firebase package so that we can interact with our database:
npm install firebase
Adding a New Firebase Cloud Firestore
Now we need to add a new Firebase Cloud Firestore — a scalable NoSQL cloud database. This can be done by selecting the Cloud Firestore link and clicking on the Create database button.
Next, we’ll select the option to start the Firestore in production mode.
Click Next. On the next screen we’ll need to select the location of our Cloud Firestore and click on the Enable button.
You’ll see a “Provisioning Cloud Firestore” message, followed by Setting up security rules, and after a short wait you’ll be redirected to the dashboard for your new project.
Adding a New Collection to the Firebase Cloud Firestore
Next, we’ll need to add a new collection to the Firebase Cloud Firestore that we just created. We can do that by clicking on the Start collection button.
We’ll need to add a name to our Collection ID. We can call it posts, as we’ll be adding and voting on posts.
Click Next. It’s now time to add a document to our collection. Each document requires an ID, so click the Auto-ID link in the first field. This should generate a unique ID.
Next, we’ll need to add the following fields:
Field | Type | Value | Screenshot |
---|---|---|---|
createdAt | timestamp | Current time | |
updatedAt | timestamp | Current time | |
title | string | This is the first post from Firebase | |
upVotesCount | number | 0 | |
downVotesCount | number | 0 |
This is what our the collection will finally look like:
Click on the Save button. The collection will be created and you’ll be redirected to the project’s dashboard.
Updating the Rules of the Firebase Cloud Firestore
If we visit the Rules tab, we’ll see the following rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
We need to modify this to allow the write operation as well:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
Finally, click on the Publish button to save our modified rules:
Note: more information regarding security rules can be found here.
Integrating Firebase with Create React App
In this section, we’ll work on our React application to add the following:
- Adding Chakra UI package
- Option to view all posts
- Option to add a new post
- Option to disable vote button once a user has voted on a post
Adding the Chakra UI Package
We’ll be adding the Chakra UI package to help us build our application’s UI. It’s a simple, modular and accessible React component library. You can check out their Getting Started guide, if you’d like to find out more.
We can install Chakra UI using the following command:
npm install @chakra-ui/core@next
For Chakra UI to work correctly, we’ll need to set up the ChakraProvider at the root of the application. Modify src/index.js
like so:
import { ChakraProvider } from "@chakra-ui/core";
import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
ReactDOM.render(
<React.StrictMode>
<ChakraProvider>
<App />
</ChakraProvider>
</React.StrictMode>,
document.getElementById("root")
);
Adding the Option to View All Posts
In this section, we’ll develop a list to show all our posts from Firebase. We’ll need to modify our src/app.js
file with the following:
import { Container, Flex, Spinner, VStack } from "@chakra-ui/core";
import React, { useEffect, useState } from "react";
import Post from "./components/post";
import db from "./lib/firebase";
const App = () => {
const [posts, setPosts] = useState([]);
useEffect(() => {
// Hook to handle the initial fetching of posts
db.collection("posts")
.orderBy("createdAt", "desc")
.get()
.then((querySnapshot) => {
const data = querySnapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setPosts(data);
});
}, []);
return (
<>
<Container maxW="md" centerContent p={8}>
<VStack spacing={8} w="100%">
{posts.map((post) => (
<Post post={post} key={post.id} />
))}
</VStack>
</Container>
</>
);
};
export default App;
Here, we’re doing the following:
- The useEffect hook is responsible for fetching the initial set of posts from Firebase. Hopefully the query syntax is relatively straightforward. You can read more about performing queries in Cloud Firestore here.
- Once the posts have been fetched from Firebase, we’re storing all the posts in the
posts
state. - We’re rendering a list of posts by using the
Post
component. - The
Post
component is responsible for handling the rendering of a single post.
Next, we’ll need to create a new file src/components/post.js
with the following content:
import { Box, HStack, Text } from "@chakra-ui/core";
import React from "react";
const Post = ({ post }) => {
return (
<HStack key={post.id} w="100%" alignItems="flex-start">
<Box bg="gray.100" p={4} rounded="md" w="100%">
<Text>{post.title}</Text>
</Box>
</HStack>
);
};
export default Post;
There’s not much going on here. The component receives the post via props and displays its title in a Chakra UI Text element.
Restart the dev server using Ctrl + C, then visit http://localhost:3000/. We should be able to view the post that we entered manually in the Firestore.
Adding the Option to Add a New Post
In this section, we’ll develop a modal through which we’ll be able to add a new post. To do that, we’ll need to add the following code to our src/app.js
file:
...
import Navbar from "./components/navbar";
...
const App = () => {
...
return (
<>
<Navbar />
<Container maxW="md" centerContent p={8}>
...
</Container>
</>
);
};
We’ll also need to add a new file src/components/navbar.js
with the following content:
import { Box, Container, Flex } from "@chakra-ui/core";
import React from "react";
import AddNewPost from "./add-new-post";
const Navbar = () => {
return (
<Box position="sticky" top={0} p={4} bg="gray.100" zIndex={1}>
<Container maxW="md" centerContent>
<Flex justifyContent="flex-end" w="100%" position="sticky" top={0}>
<AddNewPost />
</Flex>
</Container>
</Box>
);
};
export default Navbar;
We’ll also need to add a new file src/components/add-new-post.js
with the following content:
import {
Button,
FormControl,
FormLabel,
Textarea,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
HStack,
useDisclosure,
} from "@chakra-ui/core";
import React, { useState, useEffect } from "react";
import db from "../lib/firebase";
const AddNewPost = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [title, setTitle] = useState("");
const [isSaving, setSaving] = useState(false);
const handleSubmit = async () => {
const date = new Date();
await db.collection("posts").add({
title,
upVotesCount: 0,
downVotesCount: 0,
createdAt: date.toUTCString(),
updatedAt: date.toUTCString(),
});
onClose();
setTitle("");
};
return (
<>
<Button onClick={onOpen} colorScheme="blue">
Add new post
</Button>
<Modal onClose={onClose} isOpen={isOpen} isCentered>
<ModalOverlay>
<ModalContent>
<ModalHeader>Add new post</ModalHeader>
<ModalCloseButton />
<ModalBody>
<FormControl id="post-title">
<FormLabel>Post title</FormLabel>
<Textarea
type="post-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</FormControl>
</ModalBody>
<ModalFooter>
<HStack spacing={4}>
<Button onClick={onClose}>Close</Button>
<Button
onClick={handleSubmit}
colorScheme="blue"
disabled={!title.trim()}
isLoading={isSaving}
>
Save
</Button>
</HStack>
</ModalFooter>
</ModalContent>
</ModalOverlay>
</Modal>
</>
);
};
export default AddNewPost;
The AddNewPost
component will be responsible for opening a modal to add a new post. We make use of Chakra’s useDisclosure hook, a custom hook to help handle common open, close, or toggle scenarios.
Now, if we visit http://localhost:3000/, we should be able to view the following:
If we click on the Add new post button, a modal will appear through which we can add a new post:
However, we’ll need to refresh the page to view the new post. We can fix that by adding a new useEffect
hook to our src/app.js
file:
// src/app.js
useEffect(() => {
// Hook to handle the real-time updating of posts whenever there is a
// change in the datastore (https://firebase.google.com/docs/firestore/query-data/listen#view_changes_between_snapshots)
db.collection("posts")
.orderBy("createdAt", "desc")
.onSnapshot((querySnapshot) => {
const _posts = [];
querySnapshot.forEach((doc) => {
_posts.push({
id: doc.id,
...doc.data(),
});
});
setPosts(_posts);
});
}, []);
Now, if we add a new post, it’ll be visible in real time.
Adding the Option to Vote on a Post
In this section, we’ll develop the buttons through which a user can vote on each post. To do that, we’ll need to add the following code to our src/components/post.js
file:
...
import VoteButtons from "./vote-buttons";
const Post = ({ post }) => {
return (
<HStack key={post.id} w="100%" alignItems="flex-start">
<VoteButtons post={post} />
...
</HStack>
);
};
export default Post;
Next, we’ll need to add a new file src/components/vote-buttons.js
with the following:
// src/components/vote-buttons.js
import { IconButton, Text, VStack } from "@chakra-ui/core";
import React, { useState } from "react";
import { FiArrowDown, FiArrowUp } from "react-icons/fi";
import db from "../lib/firebase";
const VoteButtons = ({ post }) => {
const handleClick = async (type) => {
// Do calculation to save the vote.
let upVotesCount = post.upVotesCount;
let downVotesCount = post.downVotesCount;
const date = new Date();
if (type === "upvote") {
upVotesCount = upVotesCount + 1;
} else {
downVotesCount = downVotesCount + 1;
}
await db.collection("posts").doc(post.id).set({
title: post.title,
upVotesCount,
downVotesCount,
createdAt: post.createdAt,
updatedAt: date.toUTCString(),
});
};
return (
<>
<VStack>
<IconButton
size="lg"
colorScheme="purple"
aria-label="Upvote"
icon={<FiArrowUp />}
onClick={() => handleClick("upvote")}
/>
<Text bg="gray.100" rounded="md" w="100%" p={1}>
{post.upVotesCount}
</Text>
</VStack>
<VStack>
<IconButton
size="lg"
colorScheme="yellow"
aria-label="Downvote"
icon={<FiArrowDown />}
onClick={() => handleClick("downvote")}
/>
<Text bg="gray.100" rounded="md" w="100%" p={1}>
{post.downVotesCount}
</Text>
</VStack>
</>
);
};
export default VoteButtons;
The VoteButtons
component is responsible for rendering an upvote and downvote button. When a user clicks on either of these two buttons, the handleClick
function is called. The handleClick
function is responsible for saving the vote to the database.
Since, we’re using the icons from React Icons, we’ll need to add the package. We can do that by running the following command from our root directory:
npm install react-icons
Now, if we visit http://localhost:3000/, we should be able to view the following:
We should be able to vote on any of the posts:
Adding the Option to Disable Vote Button Once User Has Voted on a Post
In the previous section, we added the option to vote on a post. However, we can see that a user can vote on a single post multiple times. We can fix that by disabling the voting button once a user has already voted on a post.
To do that, we’ll need to add the following code to our src/component/vote-buttons.js
file:
import React, { useEffect, useState } from "react";
...
const VoteButtons = ({ post }) => {
const [isVoting, setVoting] = useState(false);
const [votedPosts, setVotedPosts] = useState([]);
useEffect(() => {
// Fetch the previously voted items from localStorage. See https://stackoverflow.com/a/52607524/1928724 on why we need "JSON.parse" and update the item on localStorage. Return "true" if the user has already voted the post.
const votesFromLocalStorage = localStorage.getItem("votes") || [];
let previousVotes = [];
try {
// Parse the value of the item from localStorage. If the value of the
// items isn't an array, then JS will throw an error.
previousVotes = JSON.parse(votesFromLocalStorage);
} catch (error) {
console.error(error);
}
setVotedPosts(previousVotes);
}, []);
const handleDisablingOfVoting = (postId) => {
// This function is responsible for disabling the voting button after a
// user has voted. Fetch the previously voted items from localStorage. See
// https://stackoverflow.com/a/52607524/1928724 on why we need "JSON.parse"
// and update the item on localStorage.
const previousVotes = votedPosts;
previousVotes.push(postId);
setVotedPosts(previousVotes);
// Update the voted items from localStorage. See https://stackoverflow.com/a/52607524/1928724 on why we need "JSON.stringify" and update the item on localStorage.
localStorage.setItem("votes", JSON.stringify(votedPosts));
};
const handleClick = async (type) => {
setVoting(true);
...
// Disable the voting button once the voting is successful.
handleDisablingOfVoting(post.id);
setVoting(true);
};
const checkIfPostIsAlreadyVoted = () => {
if (votedPosts.indexOf(post.id) > -1) {
return true;
} else {
return false;
}
};
return (
<>
<VStack>
<IconButton
...
isLoading={isVoting}
isDisabled={checkIfPostIsAlreadyVoted()}
/>
...
</VStack>
<VStack>
<IconButton
...
isLoading={isVoting}
isDisabled={checkIfPostIsAlreadyVoted()}
/>
...
</VStack>
</>
);
};
export default VoteButtons;
In the above changes, we’re doing the following:
- We’re keeping track of the
id
the posts that have been voted in our localStorage. - After a post has been voted upon, we’re adding the
id
of that post to ourlocalStorage
. - We’re disabling the vote buttons after a user votes on the post. When the app renders any voted on posts will be disabled by default.
Please note that normally you would store this kind of information in a database. Unfortunately, this is outside of the scope of our app, as that would mean we would need to implement a whole user management and authentication system.
Pushing Our Code Changes to GitHub
We’re now done with adding all the features to our application. In this section, we’ll commit our code and push it to GitHub.
Creating a GitHub Account
As we’re going to be storing our code on GitHub, we’ll need a GitHub account. Please note that this will be required when we come to deploy the application on Vercel.
Committing Our Code Using Git
You’ll need Git installed on your PC for this next step. If you’re unfamiliar with Git, or you’d like a refresher, check out Jump Start Git, 2nd Edition over on SitePoint Premium.
From our root directory, we can run the following commands to stage all our files:
git add --all
Note: more information on git add
is available here.
Next, we can commit our files using the following command:
git commit -m "Adds all the necessary code"
Note: more information on git commit
is available here.
Creating a New GitHub Repository
We can create a new GitHub repository by visiting https://github.com/new.
Once we add a name to our repository, we can click on the Create repository button to create a new repository.
Pushing the Code to Our GitHub Repository
We can push the code to our GitHub repository using the following command:
git remote add origin https://github.com/ghoshnirmalya/reddit-clone-app.git
git branch -M main
git push -u origin main
Note: you’ll need to replace “https://github.com/sitepoint-editors/reddit-clone.git” with the link of your GitHub repository.
And that’s it. Our application is now under version control and pushed up to GitHub!
Deploying the Application to Vercel
In this final section, we’ll deploy our code to Vercel.
Creating a Vercel Account
First, head over to Vercel and create an account. You can sign in with GitHub, GitLab and BitBucket.
Importing a Git Repository to Vercel
We can import our GitHub repository from GitHub by clicking on the Continue button in the Import Git Repository section.
Next, we’ll need to enter the link to our GitHub project and click on the Continue button to deploy our application.
So that our React app can communicate with our back end, we’ll need to enter all the environment variables from our .env
file.
It should contain the environment variables.
Next, we can click on the Deploy button, which will deploy the application.
If we now visit the deployment link, we should be able to view our deployed application:
Conclusion
The live demo of our application is deployed on Vercel and the code is available on GitHub.
We didn’t add any authentication, in order to reduce the complexity and the length of the tutorial, but obviously any real-world application would require it.
Firebase is really useful for places where you don’t want to create and maintain a separate back-end application, or where you want real-time data without investing too much time developing your APIs.
I hope this tutorial helps you in your future projects. Please feel free to reach out with any feedback.
Frequently Asked Questions (FAQs) about Building a Reddit Clone with React and Firebase
How can I integrate Firebase with my React application?
Integrating Firebase with your React application involves a few steps. First, you need to create a Firebase project from the Firebase console. After creating the project, you’ll be provided with a configuration object. This object contains keys and values that you need to initialize Firebase in your React application. You can use the firebase npm package to integrate Firebase into your React application. Import the firebase module and initialize it with your configuration object. Now, you can use Firebase services like Firestore, Authentication, and Storage in your React application.
How can I handle user authentication in my Reddit clone?
Firebase provides a robust authentication service that supports email and password authentication, Google Sign-In, and many other providers. You can use Firebase Authentication to handle user sign-up, sign-in, and sign-out in your Reddit clone. Firebase also provides a way to maintain a user session. So, you can keep a user signed in even after they refresh the page or close the app.
How can I structure my Firestore database for a Reddit clone?
Structuring your Firestore database depends on the features you want to implement. For a basic Reddit clone, you might need collections for users, posts, and comments. Each post document can contain fields like title, body, author, and timestamp. Each comment document can contain fields like body, author, post, and timestamp. You can use Firestore’s subcollections feature to organize comments under posts.
How can I implement upvoting and downvoting functionality?
You can implement upvoting and downvoting functionality by adding fields to your post and comment documents in Firestore. You can have fields like upvotes and downvotes, which are arrays of user IDs. When a user upvotes a post or comment, you add their ID to the upvotes array. When they downvote, you add their ID to the downvotes array. To calculate the score of a post or comment, you subtract the length of the downvotes array from the length of the upvotes array.
How can I display posts in order of their score?
Firestore provides a way to order documents in a collection by a specific field. You can use this feature to display posts in order of their score. First, you calculate the score of each post by subtracting the length of the downvotes array from the length of the upvotes array. Then, you order the posts collection by the score field in descending order. This way, posts with a higher score appear first.
How can I handle real-time updates in my Reddit clone?
Firestore provides real-time listeners that you can use to listen for changes in your database. When a change occurs, Firestore sends an update to your app with the new data. You can use this feature to update your app in real time when a new post is added, a post is upvoted or downvoted, or a comment is added to a post.
How can I handle routing in my Reddit clone?
You can use React Router to handle routing in your Reddit clone. React Router is a popular library for routing in React applications. It allows you to define routes for different components in your app. For example, you can define a route for the home page, a route for a specific post, and a route for a user’s profile.
How can I handle form validation in my Reddit clone?
You can handle form validation using React’s state and events. For example, you can use state to keep track of the form’s values and whether each field is valid. You can use events to validate each field when the user types into it or submits the form. If a field is invalid, you can display an error message and prevent the form from being submitted.
How can I style my Reddit clone?
You can style your Reddit clone using CSS. React supports both regular CSS and CSS-in-JS libraries like styled-components. You can use regular CSS by importing a CSS file into your component file. You can use styled-components by defining styled components in your component file and using them in your JSX.
How can I deploy my Reddit clone?
You can deploy your Reddit clone using Firebase Hosting. Firebase Hosting is a static web hosting service that serves your app’s static files (HTML, CSS, JavaScript) from a global CDN with HTTP/2. To deploy your app, you need to build it for production, initialize Firebase in your project directory, and deploy it using the Firebase CLI.
I'm a computer science engineer specializing in web design and development with an eye for detail. I love working with React.js.