Adding Live Comments to Your Gatsby Site

May 28, 2020

How to add a live comment section to your Gatsby site with Hasura.

This tutorial assumes a basic knowledge of Gatsby and GraphQL and already have an existing Gatsby site that you want to add your comment feed to. Familiarity with Postgres/Hasura and Apollo are also helpful.

Motivation

I recently played around with Gatsby themes for the first time. I found this cool Gatsby theme that combines MDX deck and gatsby-theme-blog. Essentially, it creates a blog based on your MDX files with a link to the actual slide deck and a blog post with any notes you want to include in the post, with the slides scrolling alongside. You can check out my forked repo here and the demo here (I made my own fork of the original theme to fix a couple of bugs).

I thought it would be cool to implement a live comment section at the bottom of each post on a different repo that shadows my version of this Gatsby theme. That way people who don't necessarily feel comfortable vocalizing their questions or comments during a live presentation could do so in a text format. I wasn't quite sure how to add a live feed into a Gatsby site, but I found a great tutorial by Jason Lengstorf and Vladimir Novick and decided to do a write up about it!

Creating the Comments Database

For this project, we'll be using Hasura with Heroku. You can find the instructions on how to set it up here.

Once you have your app set up, open your Heroku app to open the Hasura dashboard and navigate to Data. Here, you can create your tables and columns for your database. You can make this however fits your needs. I created a simple database with just one comments table with columns for the comment id, the name of the person commenting, the actual comment itself, the post_id for the blog post the comment is for, and a time stamp for time_posted. I didn't care about creating a users table to help keep track of all of the comments tied to a specific user, so I just left it at that.

For the primary key id for each comment, select UUID for the column_type and gen_random_uuid() for the default_value. This will automatically generate random ids for each of the rows in your table.

In the Permissions tab for your table, create a new role. This will be the 'role' used to limit the access that a user has within the database. After all, you don't want anyone and everyone to be able to have full read and write access of your database. So we can create a new public or commenter role (or whatever you want to name yours) and only give permissions for the things a user needs to be able to read and write comments. Under insert and select, give full column insert permissions so that a user can add a new comment to and read comments from the database. There should already be an admin role that has full permissions for everything.

After you finish creating your table, you can start adding some test comments so we can check to see if things are working. You can either do this by navigating to your newly created table by clicking on it on the left side of the screen and using the GUI or you can go to the GraphiQL tab and add them via mutations. Here is what it looks like with the GUI:

**Note that the post_id will have to be a real id of one of your blog posts

Now in the Heroku settings for your app, add a Config Var called x-hasura-admin-secret. The value can be whatever you want it to be. It's your secret :)

Now your database is all set up! Hang on to that public / commenter role name you created earlier. We'll need that in the next section.

Client-side Setup

On the client-side, we will be using Apollo to handle the requests to our newly created database.

There are quite a few packages that we need to install for this. Unfortunately apollo-boost doesn't quite have everything we need, so we have to install them all individually.

SHELL
npm i --save @apollo/react-hooks apollo-cache-inmemory apollo-client apollo-link apollo-link-ws apollo-link-http apollo-utilities graphql-tag isomorphic-fetch subscriptions-transport-ws ws

Then from the root of your project, navigate to src/utils. Create a utils folder if you don't already have one. In here, create a file called apollo.js with the following code:

src/utils/apollo.jsJSX
1import ApolloClient from "apollo-client"
2import fetch from "isomorphic-fetch"
3import React from "react"
4import { ApolloProvider } from "@apollo/react-hooks"
5import { split } from "apollo-link"
6import { HttpLink } from "apollo-link-http"
7import { WebSocketLink } from "apollo-link-ws"
8import { InMemoryCache } from "apollo-cache-inmemory"
9import { SubscriptionClient } from "subscriptions-transport-ws"
10import { getMainDefinition } from "apollo-utilities"
11import ws from "ws"
12
13const http = new HttpLink({
14 uri: "https://YOUR-HEROKU-APP-NAME.herokuapp.com/v1/graphql",
15 headers: {
16 "x-hasura-role": "YOUR-USER-ROLE-HERE",
17 },
18 fetch,
19})
20
21const wsForNode = typeof window === "undefined" ? ws : null
22
23const wsClient = new SubscriptionClient(
24 "wss://YOUR-HEROKU-APP-NAME.herokuapp.com/v1/graphql",
25 {
26 reconnect: true,
27 connectionParams: () => ({
28 headers: {
29 "x-hasura-role": "YOUR-USER-ROLE-HERE",
30 },
31 }),
32 },
33 wsForNode
34)
35
36const websocket = new WebSocketLink(wsClient)
37
38const link = split(
39 ({ query }) => {
40 const { kind, operation } = getMainDefinition(query)
41
42 return kind === "OperationDefinition" && operation === "subscription"
43 },
44 websocket,
45 http
46)
47
48export const client = new ApolloClient({ link, cache: new InMemoryCache() })
49
50export const wrapRootElement = ({ element }) => (
51 <ApolloProvider client={client}>{element}</ApolloProvider>
52)

Replace YOUR-HEROKU-APP-NAME with the name of your database app name and YOUR-USER-ROLE-HERE with the name of the role you created that has limited permissions to access the database.

For brevity's sake, I won't go into a lot of detail. But essentially what's happening is you're creating an HTTP link that can be used for your mutations and then using a web socket to be able to use a subscription to get the live updates when a new comment is added to the database. Check out Jason Lengstorf's video for more details.

Now that our apollo utility file is ready, we need to create 2 files in the root of our project: gatsby-browser.js and gatsby-ssr.js. The contents of these files will be the exact same:

gatsby-browser.jsJS
// add this to your `gatsby-ssr.js` file, too
export { wrapRootElement } from "./src/utils/apollo"

What this does is it makes sure that our application is wrapped in our Apollo Provider both for the server-side rendering and the browser.

Pulling the comments into components

Now that our Apollo Provider is all set up, we can start pulling in our data into our components! Here is a simplified overview of my component structure:

src/components/Post.jsJSX
<Post>
... {/* Post content */}
<Comments id={post.id}>
{comments.map(comment => <Comment />}
<CommentForm id={id}/> {/* this id is the post.id we're drilling down from Post */}
</Comments>
</Post>

Since my blog posts are coming from a different source than my comments, I decided to tie comments to the correct post by passing in the existing id from my post source into my Comments component.

Inside my Comments component, I imported the following:

src/components/Comments.jsJS
import gql from "graphql-tag"
import { useSubscription } from "@apollo/react-hooks"

Then we want to create a subscription for the comments for that particular post.

src/components/Comments.jsJS
const GET_COMMENTS = gql`
subscription($id: uuid!) {
comments(where: { post_id: { _eq: $id } }) {
id
name
comment
time_posted
post_id
}
}
`

$id is the post id that Comments is receiving from props. We can use it to filter the comments from our comments database that have that same id for the comment post_id column.

Then inside of our component, we can do the following:

src/components/Comments.jsJSX
export default Comments = ({ id }) => {
const { data, loading, error } = useSubscription(GET_COMMENTS, {
variables: { id },
})
if (loading) {
return <p>Loading...</p>
}
if (error) {
return <pre>{JSON.stringify(error, null, 2)}</pre>
}
return (
<div>
<h3>Comments</h3>
<ul>
{data.comments.map(({ id, comment, name, time_posted }) => (
<Comment
key={id}
comment={comment}
name={name}
time_posted={time_posted}
/>
))}
</ul>
<CommentForm id={id} />
</div>
)
}

To reiterate, the id that's coming in via props is the id for the blog post that these comments are tied to. We can use the useSubscription hook to grab the comments via data, as well as a built-in loading and error that you can use for error handling and transitions while you wait for the data to come in. Any time a new comment gets added to our comments database that has a matching post_id, our subscription will listen for those changes and automatically update our data. This all happens over web sockets (where as mutations happen over Http), which is why we need it in our apollo.js utility. Once we have our comments data, we can map over it to create a Comment component for each comment.

Let's look at the Comment component next:

src/components/Comment.jsJSX
export default Comment = ({ comment, name, time_posted }) => (
<div>
<p>{name}</p>
<p>{time_posted}</p>
<p>{comment}</p>
</div>
)

I've stripped out any styling for simplicity, but essentially we're passing in the data we want to display for each comment via props from the Comments component. Easy peasy!

Adding a Comment Form

But wait, what's the point of having these comment components if we can't even add new comments? Let's build out our comment form next!

Import the following at the top of your CommentForm:

src/components/CommentForm.jsJS
import gql from "graphql-tag"
import { useMutation } from "@apollo/react-hooks"

Now, much like the subscription we added to our Comments component, here we'll want to add a mutation.

src/components/CommentForm.jsJS
const ADD_COMMENT = gql`
mutation addComment($name: String!, $comment: String!, $post_id: uuid!) {
insert_comments(
objects: { name: $name, comment: $comment, post_id: $post_id }
) {
returning {
id
}
}
}
`

$name is the name of the person filling out the form, which will correspond with the name column we created in our database. Likewise with $comment. $post_id is the id of the post that our CommentForm component is receiving via props from Comments.

Then in our CommentForm component, we can use the useMutation hook that we imported. Then we can use addComment in our handleSubmit function for our form to send the comment data to the database!

src/components/CommentForm.jsJS
const [addComment] = useMutation(ADD_COMMENT)
const [formValues, setFormValues] = useState({
name: "",
comment: "",
post_id: id
})
...
const handleSubmit = (e) => {
e.preventDefault()
addComment({
variables: {
name: formValues.name,
comment: formValues.comment,
post_id: id,
}
})
// reset the form after submitting
setFormValues({
name: "",
comment: "",
post_id: id
})
}

Assuming you're using hooks, we can initialize the values for our comment form with useState and set the name and comment to empty strings and the post_id to the id of the post passed in via props. Then you create your form in such a way that the inputs update the formValues based on user input. That way when you go to submit the form, you have all of the input values accessible within your component state. Invoke addComment that you created earlier via the useMutation hook and pass in the form data as the value of a key called variables. This lets the mutation know what values are being assigned to which variables in the mutation so it can properly create a new row in the comments database.

And you're done! Test out your new live comments feed by adding a new comment to one of your posts :)

Additional Resources