Adding utterances comments to your React Blog

Akhila Ariyachandra - Feb 28 '22 - - Dev Community

Originally posted in my blog.

In this post I'll go through how I added comments to my blog using utterances, which uses GitHub issues to store the comments so it's really easy to setup.

First thing you need to have a public GitHub repository with the utterances app installed. In my case I have installed it in the repo of my blog.

Next create a component.

// Comments.tsx
import type { FC } from "react";

const Comments: FC = () => {
  return <></>;
};

export default Comments;
Enter fullscreen mode Exit fullscreen mode

After that, add a div as a container for the comments and also store it's ref.

// Comments.tsx
import type { FC } from "react";
import { useRef } from "react";

const Comments: FC = () => {
  const parentRef = useRef<HTMLDivElement>(null);

  return (
    <>
      <div ref={parentRef} />
    </>
  );
};

export default Comments;
Enter fullscreen mode Exit fullscreen mode

Then we'll add a <script> tag using an useEffect hook. utterances gives us the HTML to just add the <script> to our file, but we'll need the cleanup function in the useEffect hook later.

// Comments.tsx
import type { FC } from "react";
import { useRef, useEffect } from "react";

const Comments: FC = () => {
  const parentRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const parent = parentRef?.current;
    const script = document.createElement("script");

    script.setAttribute("src", "https://utteranc.es/client.js");
    script.setAttribute("repo", "akhila-ariyachandra/akhilaariyachandra.com");
    script.setAttribute("issue-term", "pathname");
    script.setAttribute("theme", "github-light");
    script.setAttribute("crossorigin", "anonymous");
    script.setAttribute("async", "true");

    parent?.appendChild(script);
  }, [parentRef]);

  return (
    <>
      <div ref={parentRef} />
    </>
  );
};

export default Comments;
Enter fullscreen mode Exit fullscreen mode

Replace the value for repo with your own repository.

All we're doing here is creating a <script> tag and adding it to the <div> container.

This will work fine as is but will create problems when running the blog in development mode with features like hot reloading and fast refresh. It will just keep adding multiple instances of utterances without removing the previous ones.

To fix this we can use the cleanup function of the useEffect hook to remove all children of the <div> container.

// Comments.tsx
import type { FC } from "react";
import { useRef, useEffect } from "react";

const Comments: FC = () => {
  const parentRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const parent = parentRef?.current;
    const script = document.createElement("script");

    script.setAttribute("src", "https://utteranc.es/client.js");
    script.setAttribute("repo", "akhila-ariyachandra/akhilaariyachandra.com");
    script.setAttribute("issue-term", "pathname");
    script.setAttribute("theme", "github-light");
    script.setAttribute("crossorigin", "anonymous");
    script.setAttribute("async", "true");

    parent?.appendChild(script);

    return () => {
      while (parent?.firstChild) {
        parent?.removeChild(parent?.lastChild);
      }
    };
  }, [parentRef]);

  return (
    <>
      <div ref={parentRef} />
    </>
  );
};

export default Comments;
Enter fullscreen mode Exit fullscreen mode

Now when the component rerenders, it will remove all the children of the container before running the script and displaying the comments again.

Since we have the cleanup function to remove the children on rerenders, we can also use it to remove the comments when the theme is switched if your site supports it.

In my site I use next-themes. If we add the theme variable to the useEffect hook's dependency array we can run the cleanup function and script again when the theme changes.

// Comments.tsx
import type { FC } from "react";
import { useRef, useEffect } from "react";
import { useTheme } from "next-themes";

const Comments: FC = () => {
  const parentRef = useRef<HTMLDivElement>(null);
  const { theme } = useTheme();

  useEffect(() => {
    const parent = parentRef?.current;
    const script = document.createElement("script");

    script.setAttribute("src", "https://utteranc.es/client.js");
    script.setAttribute("repo", "akhila-ariyachandra/akhilaariyachandra.com");
    script.setAttribute("issue-term", "pathname");
    script.setAttribute(
      "theme",
      theme === "dark" ? "github-dark" : "github-light"
    );
    script.setAttribute("crossorigin", "anonymous");
    script.setAttribute("async", "true");

    parent?.appendChild(script);

    return () => {
      while (parent?.firstChild) {
        parent?.removeChild(parent?.lastChild);
      }
    };
  }, [parentRef, theme]);

  return (
    <>
      <div ref={parentRef} />
    </>
  );
};

export default Comments;
Enter fullscreen mode Exit fullscreen mode

Finally as a bonus we can improve the loading speed of the script by preloading it. All we need to do is add a <link> tag the <head> tag with rel="preload".

In Next.js we can do this with the next/head component. If you're not using Next.js, you can use something like React Helmet.

// Comments.tsx
import Head from "next/head";
import type { FC } from "react";
import { useRef, useEffect } from "react";
import { useTheme } from "next-themes";

const Comments: FC = () => {
  const parentRef = useRef<HTMLDivElement>(null);
  const { theme } = useTheme();

  useEffect(() => {
    const parent = parentRef?.current;
    const script = document.createElement("script");

    script.setAttribute("src", "https://utteranc.es/client.js");
    script.setAttribute("repo", "akhila-ariyachandra/akhilaariyachandra.com");
    script.setAttribute("issue-term", "pathname");
    script.setAttribute(
      "theme",
      theme === "dark" ? "github-dark" : "github-light"
    );
    script.setAttribute("crossorigin", "anonymous");
    script.setAttribute("async", "true");

    parent?.appendChild(script);

    return () => {
      while (parent?.firstChild) {
        parent?.removeChild(parent?.lastChild);
      }
    };
  }, [parentRef, theme]);

  return (
    <>
      <Head>
        <link rel="preload" href="https://utteranc.es/client.js" as="script" />
      </Head>

      <div ref={parentRef} />
    </>
  );
};

export default Comments;
Enter fullscreen mode Exit fullscreen mode

You can check how I've implemented it my blog here.

. . . . . . . . . . . . . . . . . . . . .