How To Build Your Own Likes and Comments System With Firebase and React
Tags: react, tutorial, firebase, tailwindcss
Published: 2021-06-22 17:36:09

One of my todo items with React apps was having performance-first dynamic comments and likes system for static websites. Why? Because it brings the capabilities of going beyond content and add functionalities which invite **user engagement**.

### Both [Cusdis](https://cusdis.com/) and [Disqus](https://disqus.com/) are not performance-friendly as they highly affect [Cumulative Layout Shift (CLS)](https://web.dev/cls/).

So I set out on creating the system with [Firebase](https://firebase.google.com/), [TailwindCSS](https://tailwindcss.com/) and [React](https://reactjs.org/). While TailwindCSS is not a compulsion, it's my go to library. Let's get started.

## Setting Up Firebase
- Install Firebase (Client Side) with the following command:
```
npm i firebase
```

- Create firebase.js with the following configuration:
```javascript
// File: @/lib/firebase.js

import 'firebase/firestore'
import firebase from 'firebase/app'

// More about firebase config on https://firebase.google.com/docs/web/setup#config-object
var firebaseConfig = {
  apiKey: '',
  authDomain: '',
  projectId: '',
  storageBucket: '',
  messagingSenderId: '',
  appId: '',
}

if (!firebase.apps.length) {
  firebase.initializeApp(firebaseConfig)
} else {
  firebase.app()
}

export const firestore = firebase.firestore()
export default firebase
```

## Creating Like Component
- Create the like.js file:
```javascript
// File: components/blog/like.js

import { firestore } from '@/lib/firebase'
```

- Add the getLikes function which takes in the slug of the blog page, and a callback function if needed.
```javascript
export const getLikes = (slug, callBackFunction) => {
  firestore
    .collection('likes')
    .doc(slug)
    .get()
    .then((doc) => {
      if (doc.exists) {
        callBackFunction(Object.keys(doc.data()).length)
      }
    })
    .catch((err) => {
      console.error(err)
    })
}
```

- Add the postLike function which takes in the slug of the blog page, and a callback function if needed.
```javascript
export const postLike = (slug, callBackFunction) => {
  fetch('https://api.ipify.org/?format=json', {
    method: 'GET',
  })
    .then((res) => res.json())
    .then((res) => {
      firestore
        .collection('likes')
        .doc(slug)
        .set(
          {
            [res['ip']]: null,
          },
          { merge: true }
        )
        .then(callBackFunction)
    })
    .catch((err) => {
      console.error(err)
    })
}
```

## Creating Comment Component
- Create the comment.js file:
```javascript
// File: components/blog/comments.js

import { useState } from 'react'
import firebase, { firestore } from '@/lib/firebase'
```

- Adding the getComments function which takes in the slug of the blog page, and a callback function if needed.
```javascript
export const getComments = (slug, callBackFunction) => {
  firestore
    .collection('comments')
    .get()
    .then((snapshot) => {
      const posts = snapshot.docs
        .map((doc) => doc.data())
        .filter((doc) => doc.slug === slug)
        .map((doc) => {
          return { id: doc.id, ...doc }
        })
      callBackFunction(posts)
    })
    .catch((err) => {
      console.log(err)
    })
}
```

- Adding the writeComment function which takes in the slug of the blog page, and a callback function if needed.
```javascript
export const writeComment = (name, slug, content, email, callBackFunction) => {
  let temp = {
    name,
    slug,
    content,
    time: firebase.firestore.Timestamp.fromDate(new Date()),
  }
  if (email.length > 0) temp['email'] = email
  firestore
    .collection('comments')
    .add(temp)
    .then(() => {
      callBackFunction()
    })
    .catch((err) => {
      console.error(err)
    })
}
```

- Creating the LoadComments function which takes in the set of the comments to display
```javascript
export const LoadComments = ({ comments }) => {
  return comments
    .sort((a, b) =>
      a.time.toDate().getTime() > b.time.toDate().getTime() ? -1 : 1
    )
    .map((item) => (
      <div
        key={item.time.seconds}
        className="border dark:border-gray-500 rounded p-5 w-full mt-5 flex flex-col"
      >
        <span className="text-lg text-gray-500 dark:text-gray-300 font-medium">
          {item.name} &middot; {item.time.toDate().toDateString()}
        </span>
        <span className="mt-3 text-md text-gray-500 dark:text-gray-300">
          {item.content}
        </span>
      </div>
    ))
}
```

- Creating the WriteComment component which takes in the slug of the blog page, and setComments for setting the new set of comments to be displayed.
```javascript
const WriteComment = ({ slug, setComments }) => {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')
  const [comment, setComment] = useState('')

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        writeComment(name, slug, comment, email, () =>
          getComments(slug, setComments)
        )
        setName('')
        setEmail('')
        setComment('')
      }}
      className="mt-10 flex flex-col w-full"
    >
      <h1 className="font-semibold text-lg">Write a comment</h1>
      <div className="flex flex-col sm:flex-row sm:space-x-5 items-start">
        <input
          required
          value={name}
          placeholder="Name*"
          onChange={(e) => setName(e.target.value)}
          className="mt-5 w-full sm:w-1/2 appearance-none outline-none ring-0 px-5 py-2 border dark:hover:border-white hover:border-black rounded hover:shadow text-black dark:bg-black dark:text-gray-300 dark:border-gray-500"
        />
        <div className="mt-5 w-full sm:w-1/2 flex flex-col space-y-1">
          <input
            value={email}
            placeholder="Email (Optional)"
            onChange={(e) => setEmail(e.target.value)}
            className="w-full appearance-none outline-none ring-0 px-5 py-2 border dark:hover:border-white hover:border-black rounded hover:shadow text-black dark:bg-black dark:text-gray-300 dark:border-gray-500"
          />
          <span className="text-sm text-gray-400">
            Email will remain confidential.
          </span>
        </div>
      </div>
      <textarea
        required
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        placeholder={'Comment*\nMaximum of 500 characters.'}
        className="mt-5 appearance-none outline-none ring-0 pt-5 px-5 pb-10 border dark:hover:border-white hover:border-black rounded hover:shadow text-black dark:bg-black dark:text-gray-300 dark:border-gray-500"
      />
      <button
        type="submit"
        className="w-[200px] appearance-none mt-5 py-2 px-5 text-center rounded border hover:bg-gray-100 dark:hover:bg-[#28282B] dark:border-gray-500"
      >
        Post a comment
      </button>
    </form>
  )
}

export default WriteComment
```

## Creating Dynamic Blog Component
- Load the components in the dynamic blog [slug].js file:
```javascript
import WriteComment, {
  getComments,
  LoadComments,
} from '@/components/blog/comments'

export default function Post({ post }) {
  const [comments, setComments] = useState([])
  return <>
    <WriteComment setComments={setComments} slug={post.slug} />
    <div className="mt-10 pt-10 w-full border-t dark:border-gray-500">
      <button
        onClick={() => getComments(post.slug, setComments)}
        className="w-[200px] appearance-none py-2 px-5 text-center rounded border hover:bg-gray-100 dark:hover:bg-[#28282B]   dark:border-gray-500"
      >
        Load Comments
      </button>
    </div>
    <LoadComments comments={comments} />
  </>
}
```

## Example
You can see an example on my [blog page](https://rishi.app/blog/how-to-build-your-own-likes-and-comments-system-with-firebase-and-react)! The source code is available [here](https://github.com/rishi-raj-jain/rishi.app/blob/master/components/blog/comments.js).