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.
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:
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"1213const 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})2021const wsForNode = typeof window === "undefined" ? ws : null2223const 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 wsForNode34)3536const websocket = new WebSocketLink(wsClient)3738const link = split(39 ({ query }) => {40 const { kind, operation } = getMainDefinition(query)4142 return kind === "OperationDefinition" && operation === "subscription"43 },44 websocket,45 http46)4748export const client = new ApolloClient({ link, cache: new InMemoryCache() })4950export 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:
// add this to your `gatsby-ssr.js` file, tooexport { 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:
<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:
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.
const GET_COMMENTS = gql`subscription($id: uuid!) {comments(where: { post_id: { _eq: $id } }) {idnamecommenttime_postedpost_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:
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 }) => (<Commentkey={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:
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
:
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.
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!
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 submittingsetFormValues({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 :)