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;
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;
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;
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;
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;
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;
You can check how I've implemented it my blog here.