SitePoint
  • Premium
  • Library
  • Community
  • Jobs
  • Blog
LoginStart Free Trial
5 Practical React Projects
Notice of Rights
Notice of Liability
Trademark Notice
About SitePoint
Who Should Read This Book
Conventions Used
Why Firebase?
Why React?
Setting up the Project
Connecting the App with Firebase
Conclusion
Prerequisites
Scaffold the App
Build the API Server with Feathers
Build the UI
Manage React State with Redux
Server-side Validation with Redux-Form
Handle Create and Update Requests using Redux-Form
Client-side Validation with Redux Form
Implement Contact Updates
Implement Delete Request
Conclusion
Redux
ImmutableJS
Demo
Setup
React and Components
Redux and Immutable
Connecting Everything
Conclusion
How It All Began
Why React?
React and WebGL
Debugging
Performance Considerations
That's It!
Making a Farm
Rendering the Farm
Summary

How to Create a Reddit Clone Using React and Firebase

In this article, we'll be using Firebase along with Create React App to build an app that will function similar to Reddit. It will allow the user to submit a new link that can then be voted on.

Here's a live demo of what we'll be building.

Why Firebase?

Using Firebase will make 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.

Using a State Container

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.

The whole project is available on GitHub.

Setting up the Project

Let's walk through the steps to set up our project structure and any necessary dependencies.

Installing create-react-app

If you haven't already, you need to install create-react-app. To do so, you can type the following in your terminal:

Code snippet

npm install -g create-react-app

Once you've installed it globally, you can use it to scaffold a React project inside any folder.

Now, let's create a new app and call it reddit-clone.

Code snippet

create-react-app reddit-clone

This will scaffold a new create-react-app project inside the reddit-clone folder. Once the bootstrapping is done, we can go inside reddit-clone directory and fire up the development server:

Code snippet

npm start

At this point, you can go to http://localhost:3000/ and see your app skeleton up and running.

Structuring the app

For maintenance, I always like to separate my containers and components. Containers are the smart components that contain the business logic of our application and manage Ajax requests. Components are simply dumb presentational components. They can have their own internal state, which can be used to control the logic of that component (e.g. showing the current state of a controlled input component).

After removing the unnecessary logo and CSS files, this is how your app should look now. We created a components folder and a containers folder. Let's move App.js inside the containers/App folder and create registerServiceWorker.js inside the utils folder.

Structuring the app

Your src/containers/App/index.js file should look like this:

Code snippet

// src/containers/App/index.js
import React, { Component } from 'react';
class App extends Component {  render() {    return (      <div className="App">        Hello World      </div>    );  }}
export default App;

Your src/index.js file should look like this:

Code snippet

// src/index.js
import React from 'react';import ReactDOM from 'react-dom';import App from './containers/App';import registerServiceWorker from './utils/registerServiceWorker';
ReactDOM.render(<App />, document.getElementById('root'));registerServiceWorker();

Go to your browser, and if everything works fine, you'll see Hello World on your screen.

You can check my commit on GitHub.

Adding react-router

React-router will help us define the routes for our app. It's very customizable and very popular in the React ecosystem.

We'll be using version 3.0.0 of react-router.

Code snippet

npm install --save react-router@3.0.0

Now, add a new file routes.js inside the src folder with the following code:

Code snippet

// routes.js
import React from 'react';import { Router, Route } from 'react-router';
import App from './containers/App';
const Routes = (props) => (  <Router {...props}>    <Route path="/" component={ App }>    </Route>  </Router>);
export default Routes;

The Router component wraps all the Route components. Based on the path prop of the Route component, the component passed to the component prop will be rendered on the page. Here, we're setting up the root URL (/) to load our App component using the Router component.

Code snippet

<Router {...props}>  <Route path="/" component={ <div>Hello World!</div> }>  </Route></Router>

The above code is also valid. For the path /, the <div>Hello World!</div> will be mounted.

Now, we need to call our routes.js file from our src/index.js file. The file should have the following content:

Code snippet

// src/index.js
import React from 'react';import ReactDOM from 'react-dom';import { browserHistory } from 'react-router';
import App from './containers/App';import Routes from './routes';import registerServiceWorker from './utils/registerServiceWorker';
ReactDOM.render(  <Routes history={browserHistory} />,  document.getElementById('root'));
registerServiceWorker();

Basically, we're mounting our Router component from our routes.js file. We pass in the history prop to it so that the routes know how to handle history tracking.

You can check my commit on GitHub.

Adding Firebase

If you don't have a Firebase account, create one now (it's free!) by going to their website. After you're done creating a new account, log in to your account and go to the console page and click on Add project.

Enter the name of your project (I'll call mine reddit-clone), choose your country, and click on the Create project button.

Now, before we proceed, we need to change the rules for the database since, by default, Firebase expects the user to be authenticated to be able to read and write data. If you select your project and click on the Database tab on the left, you'll be able to see your database. You need to click on the Rules tab on the top that will redirect us to a screen which will have the following data:

Code snippet

{  "rules": {    ".read": "auth != null",    ".write": "auth != null"  }}

We need to change this to the following:

Code snippet

{  "rules": {    ".read": "auth === null",    ".write": "auth === null"  }}

This will let users update the database without logging in. If we implemented a flow in which we had authentication before making updates to the database, we would need the default rules provided by Firebase. To keep this application simple, we won't be doing authentication.

You Must Make This Modification

If you don't make this modification, Firebase won't let you update the database from your app.

Now, let's add the firebase npm module to our app by running the following code:

Code snippet

npm install --save firebase

Next, import that module in your App/index.js file as:

Code snippet

// App/index.js
import * as firebase from "firebase";

When we select our project after logging in to Firebase, we'll get an option Add Firebase to your web app.

Add Firebase to your web app

If we click on that option, a modal will appear that will show us the config variable which we will use in our componentWillMount method.

Configs

Let's create the Firebase config file. We'll call this file firebase-config.js, and it will contain all the configs necessary to connect our app with Firebase:

Code snippet

// App/firebase-config.js
export default {  apiKey: "AIzaSyBRExKF0cHylh_wFLcd8Vxugj0UQRpq8oc",  authDomain: "reddit-clone-53da5.firebaseapp.com",  databaseURL: "https://reddit-clone-53da5.firebaseio.com",  projectId: "reddit-clone-53da5",  storageBucket: "reddit-clone-53da5.appspot.com",  messagingSenderId: "490290211297"};

We'll import our Firebase config into App/index.js:

Code snippet

// App/index.js
import config from './firebase-config';

We'll initialize our Firebase database connection in the constructor.

Code snippet

// App/index.js
constructor() {  super();
  // Initialize Firebase  firebase.initializeApp(config);}

In the componentWillMount() lifecycle hook, we use the package firebase we just installed and call its initializeApp method and passed the config variable to it. This object contains all the data about our app. The initializeApp method will connect our application to our Firebase database so that we can read and write data.

Let's add some data to Firebase to check if our configuration is correct. Go to the Database tab and add the following structure to your database:

Test data

Clicking on Add will save the data to our database.

Demo data

Now, let's add some code to our componentWillMount method to make the data appear on our screen:

Code snippet

// App/index.js
componentWillMount() {  …
  let postsRef = firebase.database().ref('posts');
  let _this = this;
  postsRef.on('value', function(snapshot) {    console.log(snapshot.val());
    _this.setState({      posts: snapshot.val(),      loading: false      });      });    }

firebase.database() gives us a reference to the database service. Using ref(), we can get a specific reference from the database. For example, if we call ref('posts'), we'll be getting the posts reference from our database and storing that reference in postsRef.

postsRef.on('value', …) gives us the updated value whenever there's any change in the database. This is very useful when we need a real-time update to our user interface based on any database events.

Using postsRef.once('value', …) will only give us the data once. This is useful for data that only needs to be loaded once and isn't expected to change frequently or require active listening.

After we get the updated value in our on() callback, we store the values in our posts state.

Now we'll see the data appearing on our console.

Sample data

Also, we'll be passing this data down to our children. So, we need to modify the render function of our App/index.js file:

Code snippet

// App/index.js
render() {  return (    <div className="App">      {this.props.children && React.cloneElement(        ➥this.props.children, {        firebaseRef: firebase.database().ref('posts'),        posts: this.state.posts,        loading: this.state.loading      })}    </div>  );}

The main objective here is to make the posts data available in all our children components, which will be passed through react-router.

We're checking if this.props.children exists or not, and if it exists we clone that element and pass all our props to all our children. This is a very efficient way of passing props to dynamic children.

Calling cloneElement will shallowly merge the already existing props in this.props.children and the props we passed here (firebaseRef, posts and loading).

Using this technique, the firebaseRef, posts and loading props will be available to all routes.

You can check my commit on GitHub.

Connecting the App with Firebase

Firebase can only store data as objects; it doesn't have any native support for arrays. We'll store the data in the following format:

Database structure

Add the data in the screenshot above manually so that you can test your views.

Add views for all the posts

Now we'll add views to show all the posts. Create a file src/containers/Posts/index.js with the following content:

Code snippet

// src/containers/Posts/index.js
import React, { Component } from 'react';
class Posts extends Component {  render() {    if (this.props.loading) {      return (        <div>          Loading…        </div>      );    }
    return (      <div className="Posts">        { this.props.posts.map((post) => {            return (              <div>                { post.title }              </div>            );        })}      </div>    );  }}
export default Posts;

Here, we're just mapping over the data and rendering it to the user interface.

Next, we need to add this to our routes.js file:

Code snippet

// routes.js
…<Router {...props}>  <Route path="/" component={ App }>    <Route path="/posts" component={ Posts } />  </Route></Router>…

This is because we want the posts to show up only on the /posts route. So we just pass the Posts component to the component prop and /posts to the path prop of the Route component of react-router.

If we go to the URL localhost:3000/posts, we'll see the posts from our Firebase database.

You can check my commit on GitHub.

Add views to write a new post

Now, let's create a view from where we can add a new post. Create a file src/containers/AddPost/index.js with the following content:

Code snippet

// src/containers/AddPost/index.js
import React, { Component } from 'react';
class AddPost extends Component {  constructor() {    super();
    this.handleChange = this.handleChange.bind(this);    this.handleSubmit = this.handleSubmit.bind(this);  }
  state = {    title: ''  };
  handleChange = (e) => {    this.setState({      title: e.target.value    });  }
  handleSubmit = (e) => {    e.preventDefault();
    this.props.firebaseRef.push({      title: this.state.title    });
    this.setState({      title: ''    });  }
  render() {    return (      <div className="AddPost">        <input          type="text"          placeholder="Write the title of your post"          onChange={ this.handleChange }          value={ this.state.title }        />        <button          type="submit"          onClick={ this.handleSubmit }        >          Submit        </button>      </div>    );  }}
export default AddPost;

Here, the handleChange method updates our state with the value present in the input box. Now, when we click on the button, the handleSubmit method is triggered. The handleSubmit method is responsible for making the API request to write to our database. We do it using the firebaseRef prop that we passed to all the children.

Code snippet

this.props.firebaseRef.push({  title: this.state.title});

The above block of code sets the current value of the title to our database.

After the new post has been stored in the database, we make the input box empty again, ready to add a new post.

Now we need to add this page to our routes:

Code snippet

// routes.js
import React from 'react';import { Router, Route } from 'react-router';
import App from './containers/App';import Posts from './containers/Posts';import AddPost from './containers/AddPost';
const Routes = (props) => (  <Router {...props}>    <Route path="/" component={ App }>      <Route path="/posts" component={ Posts } />      <Route path="/add-post" component={ AddPost } />    </Route>  </Router>);
export default Routes;

Here, we just added the /add-post route so that we can add a new post from that route. Hence, we passed the AddPost component to its component prop.

Also, let's modify the render method of our src/containers/Posts/index.js file so that it can iterate over objects instead of arrays (since Firebase doesn't store arrays).

Code snippet

// src/containers/Posts/index.js
render() {    let posts = this.props.posts;
    if (this.props.loading) {      return (        <div>          Loading...        </div>      );    }
    return (      <div className="Posts">        { Object.keys(posts).map(function(key) {            return (              <div key={key}>                { posts[key].title }              </div>            );        })}      </div>    );  }

Now, if we go to localhost:3000/add-post, we can add a new post. After clicking on the submit button, the new post will appear immediately on the posts page.

You can check my commit on GitHub.

Implement voting

Now we need to allow users to vote on a post. For that, let's modify the render method of our src/containers/App/index.js:

Code snippet

// src/containers/App/index.js
render() {  return (    <div className="App">      {this.props.children && React.cloneElement(this.props.        ➥children, {        // https://github.com/ReactTraining/react-router/blob/v3/        ➥examples/passing-props-to-children/app.js#L56-L58        firebase: firebase.database(),        posts: this.state.posts,        loading: this.state.loading      })}    </div>  );}

We changed the firebase prop from firebaseRef: firebase.database().ref('posts') to firebase: firebase.database() because we'll be using Firebase's set method to update our voting count. In this way, if we had more Firebase refs, it would be very easy for us to handle them by using only the firebase prop.

Before proceeding with the voting, let's modify the handleSubmit method in our src/containers/AddPost/index.js file a little bit:

Code snippet

// src/containers/AddPost/index.js
handleSubmit = (e) => {  …  this.props.firebase.ref('posts').push({    title: this.state.title,    upvote: 0,    downvote: 0  });  …}

We renamed our firebaseRef prop to firebase prop. So, we change the this.props.firebaseRef.push to this.props.firebase.ref('posts').push.

Now we need to modify our src/containers/Posts/index.js file to accommodate the voting.

The render method should be modified to this:

Code snippet

// src/containers/Posts/index.js
render() {  let posts = this.props.posts;  let _this = this;
  if (!posts) {    return false;  }
  if (this.props.loading) {    return (      <div>        Loading...      </div>    );  }
  return (    <div className="Posts">      { Object.keys(posts).map(function(key) {          return (            <div key={key}>              <div>Title: { posts[key].title }</div>              <div>Upvotes: { posts[key].upvote }</div>              <div>Downvotes: { posts[key].downvote }</div>              <div>                <button                  onClick={ _this.handleUpvote.bind(this,                     ➥posts[key], key) }                  type="button"                >                  Upvote                </button>                <button                  onClick={ _this.handleDownvote.bind(this,                     ➥posts[key], key) }                  type="button"                >                  Downvote                </button>              </div>            </div>          );      })}    </div>  );}

When the buttons are clicked, the upvote or downvote count will be incremented in our Firebase DB. To handle that logic, we create two more methods: handleUpvote() and handleDownvote():

Code snippet

// src/containers/Posts/index.js
handleUpvote = (post, key) => {  this.props.firebase.ref('posts/' + key).set({    title: post.title,    upvote: post.upvote + 1,    downvote: post.downvote  });}
handleDownvote = (post, key) => {  this.props.firebase.ref('posts/' + key).set({    title: post.title,    upvote: post.upvote,    downvote: post.downvote + 1  });}

In these two methods, whenever a user clicks on either of the buttons, the respective count is incremented in the database and is instantly updated in the browser.

If we open two tabs with localhost:3000/posts and click on the voting buttons of the posts, we'll see each of the tabs get updated almost instantly. This is the magic of using a real-time database like Firebase.

You can check my commit on GitHub.

In the repository, I've added the /posts route to the IndexRoute of the application just to show the posts on localhost:3000 by default. You can check that commit on GitHub.

Conclusion

The end result is admittedly a bit barebones, as we didn't try to implement any design (although the demo has some basic styles added). We also 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. It plays really well with React, as you can hopefully see from the article.

Further reading

  • Getting React Projects Ready Fast with Pre-configured Builds
  • Build a React Application with User Login and Authentication
  • Firebase Authentication for Web
  • Leveling Up With React: React Router
End of PreviewSign Up to unlock the rest of this title.

Community Questions