Adding WordPress comments to Gatsby


The issue

When I first put this site on Gatsby, I found adding comments tricky. I used this blog post, Dynamic Comments with Gatsby and WordPress, tweaked it a bit and shoved it in. I only used it for the comments form because I wasn’t worried about rebuilding – I really just wanted to see how it worked.

But I didn’t understand it and I didn’t like wrapping my site in an ApolloClient, it seemed to slow it down a bit, and it all seemed a bit much to get not a lot of comments.

Enter this week as I’m doing something else and came across this page in the Gatsby docs, Build time and client runtime data fetching, and read about using fetch. Fetch! I know how to use that. Well… I know how to use that with the WordPress REST API. And you know what? That will do.

A high level overview

I’m keeping the posts the same as before but dynamically loading comments and handling comment submissions using fetch and the WordPress REST API.

When getting the comments, the fetch requests are wrapped in useEffect to get them after the page load, making sure to unsubscribe to the fetch Promise, and I’m using useState to set the comment list.

Submitting the comment is handled in an onclick function which sends a POST request to the WordPress comments endpoint. I allow anonymous comments (and don’t have or want logged in users submitting comments) so there’s a tiny plugin with Gatsby helpers to which I’ve added a filter to allow anonymous comments which are off by default.

The details

There are a few components used.

I’ll explain the ones with the requests in detail here, Comments and CommentForm.

The Comments component

The entire component is at the end of this. It uses fetch as an async function in useEffect. This async function is immediately called to get the comments from the site. It’s done this way because useEffect won’t take an async function like useEffect( async () => ... so this needs to be inside the function it does use eg :

JSX
const [things, setThings] = useState([])
useEffect( () => {
  // this is the async function which will return a promise to get the data.
  async function getThingsFunction () {
    const things = await fetch( endpoint )
        . then( (res) => {
                // do stuff, return the response in the correct form
                return data
         }
    return things
  }
  // (cleanup not included here, keep reading)
  // call the async function to set the state
 getThingsFunction().then( (data) => { setThings( data ) })


The fetch response in the Comments component sets the comments state which will either be an array of the comments or, if there aren’t any or there was an error, an empty array.

The request in this function is a simple GET request to the comments endpoint with a query variable post=postId to tell it which post’s comments it should get. This endpoint only sends the published comments and is public by default. The comments come as an array of objects. The WP REST API developer handbook is not the best, but you can see the schema here – it will be the “view” context that is returned.

There is a cleanup function in useEffect to avoid the “… To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function” error. The explanation for this I used it here in “Cancelling a Promise with React.useEffect” which explains things better than I could.

JSX
/** @jsx jsx */
import { jsx } from 'theme-ui'
import React, { useState, useEffect } from 'react'
import CommentsCount from './CommentsCount'
import CommentsList from './CommentsList'
import CommentForm from './CommentForm'

const Comments = ({ post, location, wordPressUrl }) => {
  // The comments endpoint, hardcoded in, I have no shame.
  const commentsEndpoint = `${wordPressUrl}/wp-json/wp/v2/comments`

  // Each comments component is attached to a post. This is the endpoint to get all comments for that post.
  const postCommentsEndpoint = `${commentsEndpoint}?post=${post.postId}`

  // Setting up state to handle the incoming comments.
  const [comments, setComments] = useState(false)
  // Do this after the page loads.
  useEffect(() => {
    // Need to wait for the comments!
    async function getPostComments() {
      const getComments = await fetch(postCommentsEndpoint)
        .then(response => {
		  // Check the response status, if it's not 200 throw an error.
          if (response.status !== 200) {
            throw Error(response.message)
          }
          return response
        })
        .then(response => {
          return response.json()
        })
        .catch(error => {
		  // Catch the error and fail quietly, no one needs to know.
          console.log(error)
          return []
        })
      return getComments
    }
    // See https://juliangaramendy.dev/use-promise-subscription/. This fixes the "To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function" error.
    let isSubscribed = true
    // Only get all the comments if this is a single page. Otherwise it runs in archive lists and that's not necessary.
    // Although! This means that until the site is rebuilt, comment counts on archive post lists will be incorrect after someone successfully
    // submits a comments. Currently I moderate all comments and really, there are almost none, so I'm ok with this for now.
    // If you wanted it up to date, you could tweak CommentsCount to use comments.length and take of the "single" check so it does
    // the request on archive pages too
    if (location === 'single') {
      getPostComments().then(postComments => {
        if (isSubscribed) {
          setComments(postComments)
        }
      })
    }
    return () => (isSubscribed = false)
  }, [])

  return (
    <>
      {location === 'single' ? (
        <>
          <CommentsList post={post} comments={comments} />
          <CommentForm post={post} commentsEndpoint={commentsEndpoint} />
        </>
      ) : (
        <CommentsCount post={post} />
      )}
    </>
  )
}

export default Comments

The CommentsForm component

This is what it says on the tin: a form for submitting comments. The form is a bog standard form. It used to be component based but I updated it to use functions and hooks a while ago and love them! I highly recommend this if you need something satisfying to do and want to sit on your computer doing it.

BEFORE YOU START

This component requires a filter in the WordPress install which allows the REST API to accept anonymous submissions. This functionality is off by default. I’m using a very simple plugin which consists of a file with this in it. (I’ll add to this plugin at some point, there is functionality elsewhere that I use which should be in it.)

PHP
<?php
/**
 * Plugin Name:     MJJ Gatsby Bits and Bobs
 * Description:     Adding little things to make the Gatsby site.
 * Author:          JJ
 * Text Domain:     mjj-gatsby
 * Domain Path:     /languages
 * Version:         0.1.0
 *
 * @package         mjjgatsbytest
 */
namespace Tharshetests\MjjGatsbyTest;

add_filter( 'rest_allow_anonymous_comments', '__return_true' );

BACK TO THE COMPONENT

The bit that handles the submission is this:

JSX
  // This handles sending the comment to the WordPress install.
  const createComment = () => {
    // Trying to avoid double clicks here.
    if (commentStatus === 'loading') {
      return // don't send this twice.
	}

	// This is a POST request to the comments endpoint. The body is sent as a JSON string.
	// Once the response is received, we set the comment status accordingly.
    fetch(commentsEndpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        author_email: email,
        author_name: userName,
        post: postId,
        content: message,
      }),
    }).then(response => {
      if (response.status === 201) {
        setCommentStatus('success')
      } else {
        setCommentStatus('error')
      }
    })
  }

The body is sent as a JSON string with the Content-Type header as application/json. It’s sent to the comments endpoint and the ID of the post for which it’s a comment is in the body. All of the parameters available are in the developer handbook.

The entire component is below. I don’t do any validation of the form fields, it’s very bare bones.

JSX
/** @jsx jsx */
import { jsx, Styled } from 'theme-ui'
import { useState } from 'react'

const CommentForm = ({ post, commentsEndpoint }) => {
  const postId = post.postId

  const [userName, setUserName] = useState('')
  const [email, setEmail] = useState('')
  const [message, setMessage] = useState('')
  const [commentStatus, setCommentStatus] = useState(false)

  // This handles sending the comment to the WordPress install.
  const createComment = () => {
    // Trying to avoid double clicks here.
    if (commentStatus === 'loading') {
      return // don't send this twice.
	}

	// This is a POST request to the comments endpoint. The body is sent as a JSON string.
	// Once the response is received, we set the comment status accordingly.
    fetch(commentsEndpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        author_email: email,
        author_name: userName,
        post: postId,
        content: message,
      }),
    }).then(response => {
      if (response.status === 201) {
        setCommentStatus('success')
      } else {
        setCommentStatus('error')
      }
    })
  }

  // Renders the comment form elements.
  const renderCommentForm = (

	// This is using Styled components. (I keep playing around with the styles, it's not my strong suit.)
	// When the form is submitted, the comment status is set to "loading", then the submission response will update it.
	// The form inputs are easy with hooks! I updated this a little while ago and love them. 💖
	// All the comments on this are up here otherwise they'll mess up the rendering.
    <div>
      <Styled.h3>Leave a comment</Styled.h3>
      <Styled.p sx={{ a: { variant: `links.decorated` }, color: `text` }}>
        Your email address is required although it will not be shown publicly.
        All comments go through Akismet whose privacy policy can be found here:{' '}
        <a href="https://akismet.com/privacy/">https://akismet.com/privacy/</a>.
        I don't use your Gravatar but will publish whatever name you put in the
        form and the date and, as you might expect, the comment. The data is
        stored on the WordPress install on WP Engine. I have to manually approve
        comments, they won't show up immediately.
      </Styled.p>
      <form
        sx={{ variant: `forms.main` }}
        onSubmit={e => {
          e.preventDefault()
          setCommentStatus('loading')
        }}
      >
        <input type="hidden" name="botField" />
        <div className="field">
          <label htmlFor="userName">Name</label>
          <input
            type="text"
            name="userName"
            id="userName"
            value={userName}
            onChange={e => setUserName(e.target.value)}
          />
        </div>
        <div className="field">
          <label htmlFor="email">Email</label>
          <input
            type="text"
            name="email"
            id="email"
            value={email}
            onChange={e => setEmail(e.target.value)}
          />
        </div>
        <div className="field">
          <label htmlFor="message">Message</label>
          <textarea
            name="message"
            id="message"
            rows="6"
            value={message}
            onChange={e => setMessage(e.target.value)}
          />
        </div>
        <ul className="actions">
          <li>
            <input
              type="submit"
              onClick={createComment}
              value="Send Message"
              className="special"
            />
          </li>
          <li>
            <input
              type="reset"
              value="Clear"
              onClick={e => {
                setMessage('')
                setCommentStatus(false)
                setUserName('')
              }}
            />
          </li>
        </ul>
      </form>
    </div>
  )

  switch (commentStatus) {
    case 'success': // A successful submission.
      return 'Your comment has been successfully submitted.'
    case 'loading': // Just submitted, no response yet.
      return 'Please wait. Your comment is being submitted.'
    case 'error': // Something went wrong.
      return 'There was an error in your submission. Please try again later.'
    default: // No submission, render the form.
      return renderCommentForm
  }
}

export default CommentForm

Summing up

I’m happy with this tbh. I have a slight feeling of weirdness about combining the REST API with GraphQL but I’m ok with that. The entire point of Gatsby is to get data from different sources and using whatever is easiest for each particular case makes sense to me.

2 responses to “Adding WordPress comments to Gatsby”

  1. That’s a very cool one! It’ll be interesting to attempt the fetch of comments at the build time and use the “fetch()” part only for the comment post interaction. That way people who only read the article and leave no comment benefit from not having to load the comments list from the WP backend.

    Thanks for getting into details about the implementation!

  2. JJ (tharsheblows)

    That’s a great idea! One thing I’ve just noticed is that my browser is caching the fetch response, so don’t worry if it takes a little longer to show up or you could add a “cache-busting” query string to the request, eg append &t=[the timestamp] to the end.

Leave a Reply

Your email address will not be published. Required fields are marked *

By submitting this comment, you are agreeing to the use of Akismet which helps reduce spam. You can view Akismet’s privacy policy here. Your email, website and name are also stored on this site.