Blog Feb 25, 2018 · 6 min read

Blog with Create React App and GitHub: Adding comments

Last post we created a simple blog using Create React App and GitHub Gists. The main reason I chose Gists instead of a repository on GitHub was so I could also allow visitors to comment. This works for technical blogs like this one.

The blog is built before pushing and we ended up removing the code that would run on the client. If we start showing comments however, we want those to be shown automatically, without the need to redeploy.

In this post I'll show one possible way to accomplish that.

Step 7: Fetching comments using the GitHub API

We add a new component for Comments and update our Post component accordingly. The logic is similar to how we fetch the content itself. By setting the Accept header to application/vnd.github.v3.html+json we'll be able to get comments already rendered to HTML.

# Post.js

 import { Helmet } from "react-helmet";
 import base64 from "base-64";
+import Comments from "./Comments";

...
   render() {
-    const { date, title } = this.props;
+    const { date, title, gist } = this.props;
     const { content } = this.state;
...
           className="markdown-body"
           dangerouslySetInnerHTML={{ __html: content }}
         />
+        <Comments gist={gist} />
+
+        <a href={`https://gist.github.com/${gist}#comments`}>
+          Write a comment on GitHub
+        </a>
       </Fragment>
     );
   }
/* Comment.js */

import React, { Component } from "react";

const headers = { Accept: "application/vnd.github.v3.html+json" };

class Comments extends Component {
  state = {
    comments: [],
  };

  fetchGistComments(id) {
    return fetch(`https://api.github.com/gists/${id}/comments`, {
      headers,
    }).then(response => response.json());
  }

  componentDidMount() {
    this.fetchGistComments(this.props.gist).then(comments =>
      this.setState({ comments })
    );
  }

  render() {
    const { comments } = this.state;

    if (comments.length === 0) {
      return null;
    }

    return (
      <section>
        <h2>Comments</h2>

        {comments.map(comment => (
          <div key={comment.id}>
            <div>
              <img
                src={`${comment.user.avatar_url}&size=40`}
                alt={`${comment.user.login} avatar`}
                width={20}
              />{" "}
              <a href={comment.user.html_url}>{comment.user.login}</a>{" "}
              <em>{comment.author_association === "OWNER" && "Author"}</em>
            </div>
            <div
              className="markdown-body"
              dangerouslySetInnerHTML={{ __html: comment.body_html }}
            />
          </div>
        ))}
      </section>
    );
  }
}

export default Comments;

Let's also remove our custom React Snap config so that the scripts are not removed from the rendered files:

# package.json

   "reactSnap": {
     "waitFor": 1000,
-    "preconnectThirdParty": false,
-    "removeScriptTags": true
+    "preconnectThirdParty": false
   }

Step 8: Reducing the size of our JavaScript bundle

This works fine now but the visitors have to download all our JavaScript code simply to see the comments. This may not be something you're worried about, but there is a simple change we can do that can help with this.

Create React App supports code splitting using dynamic imports. To keep things simple we'll npm install react-loadable and separate the code that renders on the client from what runs on the server.

# Post.js

+import snapshot from "./snapshot";
 import Comments from "./Comments";

...
       dangerouslySetInnerHTML={{ __html: content }}
     />
+
+    <div id="comments-root" data-gist={gist} />

-    <Comments gist={gist} />
+    {!snapshot && <Comments gist={gist} />}

     <a href={`https://gist.github.com/${gist}#comments`}>
       Write a comment on GitHub
/* index.js */

 import React from "react";
 import ReactDOM from "react-dom";
-import App from "./App";
+import Loadable from "react-loadable";
+
+import snapshot from "./snapshot";
 import "./index.css";

-ReactDOM.render(<App />, document.getElementById("root"));
+const production = process.env.NODE_ENV === "production";
+
+const LoadableApp = Loadable({
+  loader: () => import("./App"),
+  loading: () => <div>Loading…</div>,
+});
+
+const LoadableComments = Loadable({
+  loader: () => import("./Comments"),
+  loading: () => <div>Loading…</div>,
+});
+
+if (snapshot || !production) {
+  const el = document.getElementById("root");
+  ReactDOM.render(<LoadableApp />, el);
+} else {
+  const el = document.getElementById("comments-root");
+  if (el) ReactDOM.render(<LoadableComments gist={el.dataset.gist} />, el);
+}
/* snapshot.js */

export default navigator.userAgent === "ReactSnap";

With this change we will end up with 3 JavaScript files. There is a file called main, with the common code such as React, them there are two chunks, one for the App and another for the Comments. Because of this structure, only the main and comments chunks will be downloaded by the user.

While doing this for my own blog I noticed React Snap was injecting a preload tag that was forcing all the 3 chunks to be downloaded. I didn't find an option to disable this so I ended up adding a postbuild script to remove them:

# package.json

     "deploy": "gh-pages -d build",
     "start": "react-scripts start",
     "build": "react-scripts build && react-snap",
+    "postbuild": "sed -i '' 's/<link href=\"[^ ]*\" rel=\"preload\" as=\"script\">//g' $(find ./build -iname *.html)",
     "test": "react-scripts test --env=jsdom",
     "eject": "react-scripts eject"

That's it! This may be good enough for you but be sure to explore other more robust solutions, such as Gatsby.

Thank you for reading. Hope you learned something new👋