Build comments section with Next.js and Supabase
Tags: nextjs, typescript, supabase, swr
Published: 2022-02-07 23:32:21

## Let's build our own comments section because... why not?

Hi yall folks! In this series, we will use sexy combination - Next.js + Supabase - to build a comment section for blogs.

![Comment section](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/57s71nd41qfi40d37u00.gif)

### Our goal

Just like every good-old Todo list tutorials, we will build simple **CRUD**(**C**reate, **R**ead, **U**pdate and **D**elete) features for our comments.

- Adding comments
- Reading comments
- Editing comments
- Deleting comments
- Replying to comments

The follow-along code can be found [here.](https://github.com/MRGRAVITY817/supabase-comments)

### What do we need?

Obviously, we'll be using those three:

- [Next.js](https://nextjs.org/): Probably the best production-ready frontend framework in this planet
- [Supabase](https://supabase.com/): Trendy BaaS(Backend as a Service) product for PostgreSQL lovers

And also some extras that will make our project super-easy:

- [TailwindCSS](https://tailwindcss.com/): CSS library that will make styling super-easy.
- [SWR](https://swr.vercel.app/): Super-simple data fetching/caching library

## Local Settting: Next.js and TailwindCSS

### Create Next App

We'll first create a basic Typescript-based Next.js application using `npx create-next-app`.

```bash:Terminal
$ npx create-next-app --typescript supabase-comments
```

When the project is created, head to the files directory and you'll see those basic files.
We won't be needing all of them, so we'll remove some of them.

```bash:Project
...node_modules
├── package.json
├── pages
│   ├── _app.tsx
│   ├── api
│   │   └── hello.ts
│   └── index.tsx
├── public              # <- Remove
│   ├── favicon.ico     # <- Remove
│   └── vercel.svg      # <- Remove
├── styles
│   ├── Home.module.css # <- Remove
│   └── globals.css
├── tsconfig.json
└── yarn.lock
```

### Add TailwindCSS for NextJS

Above done, then we'll add TailwindCSS & other dependencies for our styling.

```bash:Terminal
$ yarn add -D tailwindcss postcss autoprefixer
$ yarn tailwindcss init -p
```

The last command will create `tailwindcss.config.js` file, which is a config javascript file for TailwindCSS.
This simple file does a lot, but right now we'll just define which files should TailwindCSS watch for.

```javascript:tailwindcss.config.js
module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

And then, replace contents in `styles/globals.css` with the code down below.

```css:styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

Now we can properly use our TailwindCSS with NextJS!
To test if it works, replace the contents in `pages/index.tsx` with the code down below.

```tsx:pages/index.tsx
import type { NextPage } from "next";
import Head from "next/head";

const Home: NextPage = () => {
  return (
    <div>
      <Head>
        <title>Comments Page</title>
      </Head>
      <div className="p-12">
        <h1 className="text-2xl font-bold">Comments!</h1>
      </div>
    </div>
  );
};

export default Home;
```

Run the following command to serve web page in development environment,

```bash:Terminal
$ yarn dev
```

... and head to http://localhost:3000 to see the bold typed **Comments!** in the blank web page.

Well done! We are done with our local settings(except SWR, but we'll install it later), so let's head to remote setting.

## Remote Setting: Supabase

### Create an organization and a project

Head to [official Supabase website](https://supabase.com/) and sign in with your Github account.

Then it will lead you to the app dashboard.
Click `New Project` and then click `+ New Organization` to create your new project team.
(If you already have an organization, feel free to skip creating one)

Then it will tell you to create a new name for your organization. Just type any cool name, and hit `Create organization`.

![Create Organization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vuo3iyow52gqcshwo71w.png)

Now for the organization we've just created, we'll create a new project which will contain SQL tables.

1. Name: I'll just name it "Master Supabase", but the naming doesn't matter.
2. Database Password: For the password, try using passgen tool to create a strong one. I normally create one from [PassGen.co](https://passgen.co/), with length longer than 14.
3. Region: Choose the nearest place where you live(as for me, Seoul).
4. Pricing Plan: Choose 'Free tier', but you can upgrade later if you want.

Once finished the form, hit `Create new project`.

![Create New Project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mmghr0clqhv62323k5o1.png)

And now we have our brand new Supabase project! Feel free to explore around, discover what you can do.

### Create `comments` table

Now we will make a SQL table called `comments`. To do that click a 'Table editor' menu in left panel of the dashboard.
If your project is new, the there will be no tables in it. Let's hit `Create a new table`.

Then it will show you a side panel to insert form for your table's settings.

1. Name: `comments`, but feel free to choose other name.
2. Description: This is optional, and I am gonna skip it this time.
3. Enable Row Level Security: This is a fancy feature in Supabase, but we'll discover it my later post. Now we just skip it.
4. Columns: We'll edit & add several columns like the image below.

Make sure you **change the type of 'id'** column into uuid!

![Create Table](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0lw4ngfnz1bf4qewsxg3.png)

If you've finished and confirmed the form, hit `Save` button.

Now we have our comments table! Let me explain each table columns' role :

- `id`: An unique identifier of the column.
- `created_at`: We'll use this to sort our comments in created order.
- `updated_at`: We'll use this and `created_at`'s difference to tell if the comment has been edited.
- `username`: This will be used to indicate user who wrote the comment.
- `payload`: Actual content of the comment.
- `reply_of`: I will save the comment's id that we are replying to.

## Create comment

### Add input form and Supabase client library

Our index page is kinda empty now, so we should make it look better. Replace the code in `pages/index.tsx` to the code down below.

```tsx:pages/index.tsx
import type { NextPage } from "next";
import Head from "next/head";

const Home: NextPage = () => {
  return (
    <div>
      <Head>
        <title>Comments Page</title>
      </Head>
			<div className="pt-36 flex justify-center">
        <div className="min-w-[600px]">
          <h1 className="text-4xl font-bold ">Comments</h1>
          <form onSubmit={onSubmit} className="mt-8 flex gap-8">
            <input type="text" placeholder="Add a comment" className="p-2 border-b focus:border-b-gray-700 w-full outline-none" />
            <button className="px-4 py-2 bg-green-500 rounded-lg text-white">Submit</button>
          </form>
        </div>
      </div>
    </div>
  );
};

export default Home;
```

We added as simple input form to create a comment. There are some styling codes added,
but since this is not a TailwindCSS tutorial I will leave the explanation to better [resource](https://tailwindcss.com/docs/utility-first).

Our form looks good, but it doesn't really do anything. To create a comment, we should do this process:

1. User type the comment in the form, then hit `Submit`.
2. We somehow send the comment data to our Supabase `comments` table.
3. We check our table, see if the data has been added.

To perform the 2nd step, we need a Supabase node client library. Run the command below to add it.

```bash:Terminal
$ yarn add @supabase/supabase-js
```

### Make our messenger for Supabase

Now we need to create a supabase client object, which is a messenger that will help us interact with Supabase.
Add these 2 lines in `pages/index.tsx`.

```tsx:pages/index.tsx
...
import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(supabaseUrl, supabaseKey);

const Home: NextPage = () => {
  return (
    <div>
...
```

To create a Supabase client object, we need two data: Supabase project URL and key.
These can be found in `Settings > Project settings > API` section in our Supabase project dashboard.

![Supabase dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b9utoxkdldfdsf7b1qi7.png)

### Understanding environment variables

These keys should be stored somewhere secure and separate.
Many of the developers save secured data as _'environment variables'_, commonly shorten as 'env vars'.

Env vars also works as 'variables' which can be set differently across the environment.
For our case, we need to define env vars for development environment, and to do that in NextJS, we use `.env.local` file.
If you want to use the same variables in production environment, you can use `.env.production` file and replace the values.

Ok, now let's create a `.env.local` file in our NextJS app's root directory.
Then copy-paste the first key (anon/public key) in the image above and save it to `NEXT_PUBLIC_SUPABASE_KEY`.
For the second key (URL key), save it to `NEXT_PUBLIC_SUPABASE_URL`.

If done correctly, it should look like this.

```text:.env.local
NEXT_PUBLIC_SUPABASE_KEY=[first-key]
NEXT_PUBLIC_SUPABASE_URL=[second-key]
```

Now what's all that `NEXT_PUBLIC_` prefix? NextJS handles env vars differently by their names:

- With `NEXT_PUBLIC_` prefix: Those are exposed in browser, which means it can be used in client-side jobs.
- Without `NEXT_PUBLIC_` prefix: Those are server-side jobs.

So that means our supabase basically use those keys in browser side. Once we define or edit our `.env.local` file, we have to restart
the development server, so go to terminal and kill the current session with `CTRL-C`, and then restart with `yarn dev`.

Since we can use our env vars now, add and edit the following lines in `pages/index.tsx`.

```tsx:pages/index.tsx
...
import { createClient } from "@supabase/supabase-js";

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? "";
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_KEY ?? "";

export const supabase = createClient(supabaseUrl, supabaseKey);

const Home: NextPage = () => {
  return (
    <div>
...
```

The extra `?? ""` after each env var initalization is to prevent them to be `undefined` type, which then `createClient` won't accept it.

### Send an `insert` request

Before we use our `supabase` messenger, we will first get user's comment payload from our input form.
To do that,

1. Add a `comment` react state for the placeholder of user's comment payload.
2. Add `onChange` function to update the comment payload in `comment` whenever the payload is changed.
3. Add `onSubmit` function to handle the submit behavior of the form.
   In our case we don't want to reload everytime when we submit the form, so we use `event.preventDefault()`.

The code change will be as following.

```tsx:pages/index.tsx
...
const Home: NextPage = () => {
  const [comment, setComment] = useState<string>("");

  const onChange = (event: ChangeEvent<HTMLInputElement>) => {
    const commentValue = event.target.value;
    setComment(commentValue);
  };

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
		console.log(comment);
  };
...
		<form onSubmit={onSubmit} className="mt-8 flex gap-8">
			<input
				onChange={onChange}
				type="text"
				placeholder="Add a comment"
				className="p-2 border-b focus:border-b-gray-700 w-full outline-none"
			/>
...
```

To test if it works, open the Devtools for your browser and head to `Console` tab, type something in the input and hit `Submit`.
If succeeded, it will show like the image below.

![Check form state](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vvzp03mx5pbqahr1rigz.png)

Now we'll use our supabase client to create a comment. In SQL table's perspective, it will basically add a new row.
Replace `onSubmit` function to code down below. Don't forget to add an `async` keyword since the supabase client api returns a `Promise`.

```tsx:pages/index.tsx
...
  const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    const { data, error } = await supabase.from("comments").insert({
      username: "hoonwee@email.com",
      payload: comment,
    });

    if (!error && data) {
		// If succeed
      window.alert("Hooray!");
    } else {
		// If failed
      window.alert(error?.message);
    }
  };
...
```

Let me disect the `await supabase` part real quick:

- `.from("comments")` : This indicates which table should be looked for, since theoretically we have lot's of table in project.
- `.insert(table_row)` : By using `insert`, we create a new row in our `comments` table with give table_row data.

  - As you can see, we have only put `username` and `payload` in our `table_row`, because the rest of the data will have it's default value.

- `const { data, error }`: Supabase will give us a response containing `data` which contains information of our action, and `error` if there's an error.

Now let's write something again and hit `Submit`. And if you have succeeded, you'll see an alert window containing **Hooray!** message.

Well that's pretty nice, but we still don't know whether our data has been sent or not.
Head to `comments` table in Supabase dashboard, and you'll see a new data row inserted.

![New data row](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ioqklijknnesdpqofafi.png)

## Read comments

### Create a comment list

Okay great, now we can create comments but we also want to display them in our web page.
To do that we'll do following steps:

1. We get all the comments from the `comments` table.
2. We display it - in list-like format.
3. We will sort then by `created_at` data to see them chronologically.

First we should add some UI for the list! Add the following code in `pages/index.tsx`.

```tsx:pages/index.tsx
...
export const supabase = createClient(supabaseUrl, supabaseKey);

interface CommentParams {
  id: string;
  created_at: string;
  updated_at: string;
  username: string;
  payload: string;
  reply_of?: string;
}

const Home: NextPage = () => {
  const [comment, setComment] = useState<string>("");
  const [commentList, setCommentList] = useState<CommentParams[]>([]);
...
          Submit
        </button>
      </form>
      <div className="flex flex-col gap-4 pt-12">
        {commentList.map((comment) => (
          <div key={comment.id} className="border rounded-md p-4">
            <p className="font-semibold mb-2">{comment.username}</p>
            <p className="font-light">{comment.payload}</p>
          </div>
        ))}
      </div>
    </div>
...
```

Let's disect this part real quick:

1. Below our `form` element we added a comment list section, iterated by `commentList` react state that we newly created.
2. The `commentList` state has a array-type of `CommentParams` interface, which contains all the column names for each object key.
3. The question mark `?` at `reply_of` field indicates that this field is optional.

### Send a `select` request

Before moving to next step, I want you to add more comments using our form

- because once we succeed to get the comments from supabase, it will look awesome when we have bunch of comments.

Once you've done adding more comments, now let add a new function called `getCommentList` that will use `supabase` client to
get all the comments. Add the code down below.

```tsx:pages/index.tsx
...
  const [commentList, setCommentList] = useState<CommentParams[]>([]);

  const getCommentList = async () => {
    const { data, error } = await supabase.from("comments").select("*");
    if (!error && data) {
      setCommentList(data);
    } else {
      setCommentList([]);
    }
  };

  useEffect(() => {
    getCommentList();
  }, []);
...
```

Right now `getCommentList` function will be called **once** only, when we first render our page.
To do that we will call our `getCommentList` in `useEffect` hook. Since the `useEffect` hook has no external dependency,
this will invoke the inner part only once when the component is rendered.

Now check our web page. It will look much like other comment sections!

![Comment list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2fqsqcjgmzposfo2adfb.png)

### Sort comments by created order

Now our Supabase client takes the comment list in `created` order,
but soon after when we edit and reply to some of the comments it would bring them by `updated` order.

So we should do a little tweak in our code to sort them.

```tsx:pages/index.tsx
...

          <div className="flex flex-col gap-4 pt-12">
            {commentList
              .sort((a, b) => {
                const aDate = new Date(a.created_at);
                const bDate = new Date(b.created_at);
                return +aDate - +bDate;
              })
              .map((comment) => (
                <div key={comment.id} className="border rounded-md p-4">
                  <p className="font-semibold mb-2">{comment.username}</p>

...

```

Breaking this down:

- We added `.sort` before doing `.map` in comment list rendering part.
- The logic inside `.sort` will arrange from the oldest to the youngest.
- The `+` sign at the beginning of `aDate` and `bDate` is for casting `Date` type to
  `number` type, since Typescript's `sort()`'s return value has to be in `number` type.

## Update and Delete Comments

### Plan a feature

We make mistakes, especially a lot when we write something in the internet.
That's why there's always a `Edit` and `Delete` section in posts and comment section in Facebook, Medium, Twitter and etc.

A properly working editing feature should have these features:

- Able to edit the payload(content) right at the comment item itself.
- Disable edit button unless payload hasn't changed.
- Indicate if this comment has been edited.

A great deleting feature should:

- Ask user if they're really gonna delete this to prevent if this was a click mistake.
- Then delete it.

### Create edit input form

For the best user experience, the input form to edit the comment should not be the same place where you add a comment but the comment list item itself.
That means we should upgrade our comment list item, so let's add the following code to do it!

```tsx:pages/index.tsx
...

const Home: NextPage = () => {
  const [comment, setComment] = useState<string>("");
  const [commentList, setCommentList] = useState<CommentParams[]>([]);
  const [editComment, setEditComment] = useState<EditCommentParams>({
    id: "",
    payload: "",
  });

  const onChangeEditComment = (event: ChangeEvent<HTMLInputElement>) => {
    const payload = event.target.value;
    setEditComment({ ...editComment, payload });
  };

  const confirmEdit = () => {
    window.alert("Confirm edit comment");
  };

  const getCommentList = async () => {
    const { data, error } = await supabase.from("comments").select("*");

...
        <div key={comment.id} className="border rounded-md p-4">
          <p className="font-semibold mb-2">{comment.username}</p>
          <div className="flex items-center gap-2 justify-between">
            {comment.id === editComment.id ? (
              <input
                type="text"
                value={editComment.payload}
                onChange={onChangeEditComment}
                className="pb-1 border-b w-full"
              />
            ) : (
              <p className="font-light">{comment.payload}</p>
            )}
            {editComment.id === comment.id ? (
              <div className="flex gap-2">
                <button type="button" onClick={confirmEdit} className="text-green-500">
                  Confirm
                </button>
                <button
                  type="button"
                  onClick={() => setEditComment({ id: "", payload: "" })}
                  className="text-gray-500"
                >
                  Cancel
                </button>
              </div>
            ) : (
              <button
                type="button"
                onClick={() => setEditComment({ id: comment.id, payload: comment.payload })}
                className="text-green-500"
              >
                Edit
              </button>
            )}
          </div>
        </div>
      ))}
    </div>

...

```

A brief analysis for this one:

- We added `editComment` state, which will set which comment to edit and to what payload it should be.
- We added two functions:
  - `onChangeEditComment`: This will watch the value of edit form input and set the payload to edit with `setEditComment`.
  - `confirmEdit`: This will send a request to Supabase to update the comment, but right now we just put silly window alert.
- We updated our comment item section to switch between 'read-mode' and 'edit-mode' with `editComment` state.

![Edit comment state](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g1bsoh07plhncc29winu.png)

### Constructing `update` request

Now only thing to do is just replacing the `confirmEdit` function to communicate with Supabase.
Replace that part with this code. I'm sure you are now getting familiar with `supabase` api.

```tsx:pages/index.tsx

...

   const confirmEdit = async () => {
    const { data, error } = await supabase
      .from("comments")
      .update({
        payload: editComment.payload,
      })
      .match({ id: editComment.id });
    if (!error && data) {
      window.alert("Edited Comment!");
    } else {
      window.alert(error?.message);
    }
  };

...

```

So apparently from this code,

- We used `update` function to update the data.
  - We only need to pass the changed part, not the whole other parts.
- Then used `match` function to target which comments should be updated.

But wait, shouldn't we update the `updated_at`?

That is correct! We will do it in Supabase Dashboard, not in our Next.js code.
We will use an actual SQL query to accomplish this, and to do that head to **SQL Editor** through navigation bar.
Then you'll see an input box to write down the SQL query. Insert the SQL query down below.

```sql
create extension if not exists moddatetime schema extensions;

create trigger handle_updated_at before update on comments
  for each row execute procedure moddatetime (updated_at);
```

So much to explain about this query, but basically it will set the `updated_at` column to the current timestamp for every update.
Hit `Run` to run the query and adapt the trigger.

![Run SQL query](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/alb5fx3z0u2lpbx7hq28.png)

Now our edit request will work like a charm. Try editing any comment, and then refresh. If succeeded then you'll see your comment edited.

### Disable `Confirm` button when comment is not edited

Currently we just allow user to click `Confirm` button whenever they press `Edit` button, without checking if the payload has changed.
This can arise two problems:

- Our `confirmEdit` function always changes `updated_at` data, so even though we've mistakenly confirm the edit without changing,
  the comment will always be marked as `edited` since there's no going back in time.
- Now this is not that critical matter, but if we were to use this in much bigger project,
  then there will be unnecessary transaction between user browser and Supabase.

To prevent this, we need to **disable** the `Confirm` button when user hasn't change their comment. Let's tweek a code a little bit.

```tsx:pages/index.tsx

...

    {editComment.id === comment.id ? (
      <>
        <button
          type="button"
          onClick={confirmEdit}
          disabled={editComment.payload === comment.payload}
          className={`${editComment.payload === comment.payload ? `text-gray-300` : `text-green-500`}`}
        >
          Confirm
        </button>
        <button
          type="button"
          onClick={() => setEditComment({ id: "", payload: "" })}
          className="text-gray-500"

...

```

Now our `Confirm` button will be disabled unless the content of the comment has been changed.

![Disable when not edited](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wws12jl2nur4y449vc3t.gif)

### Indicate `edited`

Comment item should indicate that it has been edited. This can be acheived quite easily - by comparing `created_at` and `updated_at`.

```tsx:pages/index.tsx

...

      .map((comment) => (
        <div key={comment.id} className="border rounded-md p-4">
          <p className="font-semibold mb-2">
            {comment.username}
            {comment.updated_at !== comment.created_at && (
              <span className="ml-4 text-sm italic font-extralight">edited</span>
            )}
          </p>
          <div className="flex items-center gap-2 justify-between">
            {comment.id === editComment.id ? (

...

```

Now if we edit any comment, it will indicate `edited` in extra-light & italic font.

### Delete comment

Deleting comment is not so different from updating comment - it uses same `match` function to target which comment should
be deleted. Let's do this real quick.

```tsx:pages/index.tsx

...

  const confirmDelete = async (id: string) => {
    const ok = window.confirm("Delete comment?");
    if (ok) {
      const { data, error } = await supabase.from("comments").delete().match({ id });
      if (!error && data) {
        window.alert("Deleted Comment :)");
      } else {
        window.alert(error?.message);
      }
    }
  };

...

                  onChange={onChangeEditComment}
                  className="pb-1 border-b w-full"
                />
              ) : (
                <p className="font-light">{comment.payload}</p>
              )}
              <div className="flex gap-2">
                {editComment.id === comment.id ? (
                  <>
                    <button type="button" onClick={confirmEdit} className="text-green-500">
                      Confirm
                    </button>
                    <button
                      type="button"
                      onClick={() => setEditComment({ id: "", payload: "" })}
                      className="text-gray-500"
                    >
                      Cancel
                    </button>
                  </>
                ) : (
                  <>
                    <button
                      type="button"
                      onClick={() => setEditComment({ id: comment.id, payload: comment.payload })}
                      className="text-green-500"
                    >
                      Edit
                    </button>
                    <button type="button" onClick={() => confirmDelete(comment.id)} className="text-gray-700">
                      Delete
                    </button>
                  </>
                )}
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>

...

```

After adding the code, click the `Delete` button in the comment item that you want to delete, press `Ok` in confirm window,
and refresh the page - then the comment is gone!

_You see, making CRUD feature with Supabase is Supa-easy!_

## Replying to Comments

### How our reply will work

Now guess the only column in our table that we haven't used yet - that's right, the `reply_of` column. We'll use it right away to
add a feature to reply comments.

Let's think how our reply feature would work best:

- User clicks `Reply` button on the comment item.
- Then our input form(for adding comments) will show that this comment will be a replying comment of some other comment.
- Once added and fetched to the comment list, it should still be distinguishable from normal comments.

### Adding `Reply of: ....`

Alrighty, as always let's work with the UI part first.

```tsx:pages/index.tsx

...

  const [editComment, setEditComment] = useState<EditCommentParams>({ id: "", payload: "" });
  const [replyOf, setReplyOf] = useState<string | null>(null);

  const onChangeEditComment = (event: ChangeEvent<HTMLInputElement>) => {

...

    <h1 className="text-4xl font-bold ">Comments</h1>
    <form onSubmit={onSubmit} className="mt-8 flex gap-8">
      <div className="w-full">
      {replyOf && (
        <div className="flex gap-4 my-2 items-center justify-start">
          <p className="text-xs font-extralight italic text-gray-600">
            Reply of: {commentList.find((comment) => comment.id === replyOf)?.payload ?? ""}
          </p>
          <button onClick={() => setReplyOf(null)} className="text-xs font-light text-red-600">
            Cancel
          </button>
        </div>
      )}

...

    .map((comment) => (
      <div key={comment.id} className="border rounded-md p-4">
        {comment.reply_of &&
          <p className="font-extralight italic text-gray-600 text-xs">
            Reply of: {commentList.find((c) => c.id === comment.reply_of)?.payload ?? ""}
          </p>
        }
        <p className="font-semibold mb-2">
          {comment.username}

...

            Delete
          </button>
          <button type="button" onClick={() => setReplyOf(comment.id)} className="text-orange-500">
            Reply
          </button>
        </>
      )}
    </div>

...

```

Here in this code, we can see that:

- We declared a new state called `replyOf` to save the id of replying comment that we select.
- We added a single text line in (1) Input form (2) Above username in comment item, showing which comment we are replying to.
  - In input form, we also added a `Cancel` button to cancel replying to it and revert our input form to normal one.
- We added a `Reply` button, which will use `setReplyOf` to save the id of comment that we select.

Ok this explanation enough, but basically it will look like this simple.

![Replying](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dcr0eekyn1l09su8ddvh.gif)

Then all you need to add is to pass the replying comment's id to `reply_of` field in `onSubmit`.

```tsx:pages/index.tsx

...

  const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const { data, error } = await supabase.from("comments").insert({
      username: "hoonwee@email.com",
      payload: comment,
      reply_of: replyOf,
    });
    if (!error && data) {
      window.alert("Hooray!");
    } else {
      window.alert(error?.message);
    }
  };

...

```

Now try adding a replying comment and then refresh. If done correctly, you'll see a reply comment like the image below.

![Replied comments](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yz24gkrkoesn6p8m8g35.png)

## Restyling using Icons

### Problems with text-only UI

So now our comment section is already awesome. It can read, create, update, delete and reply to comments.
While it's fully functional, we have to admit that it looks very boring and visually unapealing - because we only used texts for our UI.

The problem with using only text for our UI can invoke bad user experiences like:

- It can be less intuitive, which will confuse users.
- If the text is too long, it can ruin the overall visual design.

So to solve this, we need some design element that can pack the meaning of the UI into strong and concise visual format.
The best one for this, as far as I know, is an **icon**.

### Text &rarr; Icons

There are tons of icon packs in internet, and in here we will use one called [Hero icons](https://heroicons.dev/).
Since it's been developed from the people behind TailwindCSS, it works best with our project.

Install Hero icons with the following command.

```bash:Terminal
$ yarn add @heroicons/react
```

Now let's start replacing several texts into Hero icons!

```tsx:pages/index.tsx

...

import { ReplyIcon, PencilIcon, TrashIcon, CheckCircleIcon, XCircleIcon, XIcon } from "@heroicons/react/outline";

...

          Cancel
        </button>
      </>
    ) : (
      <>
        <button
          onClick={() => setEditComment({ id: comment.id, payload: comment.payload })}
          title="Edit comment"
        >
          <PencilIcon className="w-6" />
        </button>
        <button onClick={() => confirmDelete(comment.id)} title="Delete comment">
          <TrashIcon className="w-6" />
        </button>
        <button onClick={() => setReplyOf(comment.id)} title="Reply to comment">
          <ReplyIcon className="w-6 rotate-180" />
        </button>
      </>
    )}
  </div>

...

```

What has been changed?

- We replaced three texts in our comment item - `Edit`, `Delete`, and `Reply`.
- We removed the unnecessary color variation between buttons cause our icons already are distinguishable.
- We added a `title` property to show what this icon means when user's mouse pointer hovers on the button.
  - I strongly advice to do this, since the infographic that we know as common sense might not be the same in other culture.
- We rotated the `Reply` icon to 180 degrees. I did it because it felt right with this angle, but you can change it if you want.

Let's keep adding more icons.

```tsx:pages/index.tsx

...

      {replyOf && (
        <div className="flex gap-4 my-2 items-center justify-start">
          <div className="flex items-center justify-start gap-2">
            <ReplyIcon className="w-4 text-gray-600 rotate-180" />
            <p className="font-extralight italic text-gray-600 text-sm">
              {commentList.find((comment) => comment.id === replyOf)?.payload ?? ""}
            </p>
          </div>
          <button onClick={() => setReplyOf(null)} title="Cancel">
            <XIcon className="w-4 text-gray-600" />
          </button>
        </div>
      )}

...

      {comment.reply_of && (
        <div className="flex items-center justify-start gap-2">
          <ReplyIcon className="w-3 text-gray-600 rotate-180" />
          <p className="font-extralight italic text-gray-600 text-xs">
            {commentList.find((c) => c.id === comment.reply_of)?.payload ?? ""}
          </p>
        </div>
      )}

...

      <div className="flex gap-2">
        {editComment.id === comment.id ? (
          <>
            <button
              type="button"
              onClick={confirmEdit}
              disabled={editComment.payload === comment.payload}
              title="Confirm"
            >
              <CheckCircleIcon
                className={`${
                  editComment.payload === comment.payload ? `text-gray-300` : `text-green-500`
                } w-6`}
              />
            </button>
            <button type="button" onClick={() => setEditComment({ id: "", payload: "" })} title="Cancel">
              <XCircleIcon className="w-6 text-gray-600" />
            </button>
          </>
        ) : (
          <>

...

```

We've changed `Reply of` to our `Reply` icon, and changed `Confirm` and `Cancel` too.
We didn't change much, but looks way better!

![Replace with Icons](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/97s27eo5j79yxr54yl1n.png)

## Implementing SWR

Finally, we will gonna fix the data fetching part, which right now user needs to reload everytime they modify(create, edit, delete) the comments.
By using amazing library called [`SWR`](https://swr.vercel.app/), we will gonna fix this problem and it will take our comment section's user experience into whole another level.

### A brief overview of SWR

There are numerous data-fetching libraries for Next.js, but one of the most popular and easiest to use is SWR.
Here's a simple example from their official document page(modified a little bit for better understanding).

```typescript
import useSWR from 'swr';

const fetcher = (url: string) => fetch(url).then((res) => res.body);

function Profile() {
  const { data, error } = useSWR('/api/user', fetcher);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}
```

This simple looking code does something beautiful.

- It uses a hook called `useSWR`, which takes 2 arguments:
  - An `url` to fetch a data.
  - An `fetcher` function that'll fetch from the given url.
- Then you can just use the the `data`, like you use a React state.

See? It's so simple! Say goodbye to the hard days where you had to use several `useState`s and `useEffect`s to manipulate and update the
remote date - which is complicated easy to make mistakes.

As the name propose, the mechanism for this comes from a HTTP cache invalidation strategy called `stale-while-revalidate`.
Explaining the details about it is beyond our article, so better check out
[this link](https://developers.google.com/web/tools/workbox/modules/workbox-strategies#stale-while-revalidate) if you're interested.

![Stale-While-Revalidate](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j5fjz9o51wamyf4edgmc.png)

### Setting SWR, and refactor APIs

Now let's install SWR with the command below.

```bash:Terminal
$ yarn add swr
```

And we will replace our old method of fetching data to new one, using `useSWR`.

But first I am pretty sure our code need some refactoring, since we already have too much API-related code in our client-side file `index.tsx`.
Thankfully, Next.js provides us a `api` directory inside `pages` directory, which you can put all kinds of API codes.

Let's make new file `pages/api/comments.ts`, with the code down below.

```typescript:pages/api/comments.ts
import { createClient } from "@supabase/supabase-js";
import { NextApiRequest, NextApiResponse } from "next";

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL + "";
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_KEY + "";

export const supabase = createClient(supabaseUrl, supabaseKey);

const CommentsApi = async (req: NextApiRequest, res: NextApiResponse) => {
  switch (req.method) {
    // Get all comments
    case "GET":
      const { data: getData, error: getError } = await supabase.from("comments").select("*");
      if (getError) {
        return res.status(500).json({ message: getError.message });
      }
      return res.status(200).json(getData);
    // Add comment
    case "POST":
      const comment = req.body;
      const { data: postData, error: postError } = await supabase.from("comments").insert(comment);
      if (postError) {
        return res.status(500).json({ message: postError.message });
      }
      return res.status(200).json(postData);
    // Edit comment
    case "PATCH":
      const { commentId: editcommentId, payload } = req.body;
      const { data: patchData, error: patchError } = await supabase
        .from("comments")
        .update({ payload })
        .eq("id", editcommentId);
      if (patchError) {
        return res.status(500).json({ message: patchError.message });
      }
      return res.status(200).json(patchData);
    // Delete comment
    case "DELETE":
      const { comment_id: deleteCommentId } = req.query;
      if (typeof deleteCommentId === "string") {
        const { data: deleteData, error: deleteError } = await supabase
          .from("comments")
          .delete()
          .eq("id", deleteCommentId + "");
        if (deleteError) {
          return res.status(500).json({ message: deleteError.message });
        }
        return res.status(200).json(deleteData);
      }
    default:
      return res.status(405).json({
        message: "Method Not Allowed",
      });
  }
};

export default CommentsApi;
```

Now that's a lot of code all of a sudden! Don't worry, I'll explain one-by-one.

- `CommentsApi` function takes `req` which is a request from the caller of this API,
  and `res` which is a response that we'll modify according to the request.
- Inside the function, we encounter a `switch` condition filter with 5 `case`s:
  - `case "GET"`: This will be called for getting comments.
  - `case "POST"`: This will be called for adding a comment.
  - `case "PATCH"`: This will be called for editing a comment.
  - `case "DELETE"`: This will be called for deleting a comment.
  - `default`: This will omit error for unsupported methods.

So what we've done is just moving the API related stuffs to this file.
Each implementation inside the `case` block is identical to ones we've written in `index.tsx`.
It uses `await supabase.from("comments").something(...)` for every case.

Now we've made our decent looking API code, how do we access to it? It's super-easy - Just fetch "/api/comments".

### Replacing 'get comments'

Now we are going to use our well organized `comments.ts` API with `useSWR` hook.
First let's replace the old implementation of getting all the comments.

Edit & Delete codes in `index.tsx` with the code below.

```typescript:pages/index.tsx

...

const fetcher = (url: string) => fetch(url, { method: "GET" }).then((res) => res.json());

const Home: NextPage = () => {
  const { data: commentList, error: commentListError } = useSWR<CommentParams[]>("/api/comments", fetcher);
  /* Deleted
	const [commentList, setCommentList] = useState<CommentParams[]>([]);
	*/
  const [comment, setComment] = useState<string>("");

...

	/* Deleted
	const getCommentList = async () => {
		const { data, error } = await supabase.from("comments").select("*");
		if (!error && data) {
			setCommentList(data);
		} else {
			setCommentList([]);
		}
	};

	useEffect(() => {
		getCommentList();
	}, []);
	*/

...

<div className="flex items-center justify-start gap-2">
	<ReplyIcon className="w-4 text-gray-600 rotate-180" />
	<p className="font-extralight italic text-gray-600 text-sm">
		{commentList?.find((comment) => comment.id === replyOf)?.payload ?? ""}
	</p>
</div>

...

	{(commentList ?? [])
		.sort((a, b) => {
			const aDate = new Date(a.created_at);

...

<div className="flex items-center justify-start gap-2">
	<ReplyIcon className="w-3 text-gray-600 rotate-180" />
	<p className="font-extralight italic text-gray-600 text-xs">
		{commentList?.find((c) => c.id === comment.reply_of)?.payload ?? ""}
	</p>
</div>

...

```

Here's what happened:

- Removed `commentList` React State, `getCommentList` function and `useEffect` which was used to update comments when data is refetched.
- Replaced that part with a single line of code(or maybe 2 or 3 lines of code depending on your formatter), using `useSWR` hook.
  - Same as an example from above, it contains `url("/api/comments")` and `fetcher`.
  - Since we are using `GET` method with `fetch`, our `GET` case in `comments.ts` is executed, which fetches the full comment list.
- Added little `?` and ` ?? []` to `commentList` when it's used for `find`ing or `sort`ing something.
  - A reason for this is because our data fetched from `useSWR` is fallible, so it counts for the chance to being a `undefined` for fetch failure.
  - So we should inform the `find` function with `?` typing that it might contain the `undefined` data.
  - For `sort` function, which doesn't tolerate `undefined`, we have to hand over at least the empty array.

We changed our code a lot, in a good way! Our comment section should work just the same.

### Replacing "add comments"

Next we'll replace 'add comment' feature. To do that we have to add another fetching function which will send a post request to our `comments.ts`.

Add `addCommentRequest` function right after `fetcher`.

```typescript:pages/index.tsx

...

const fetcher = (url: string) => fetch(url, { method: "GET" }).then((res) => res.json());

const addCommentRequest = (url: string, data: any) =>
  fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  }).then((res) => res.json());

const Home: NextPage = () => {

...

```

We stringify the comment data and post it. No difficult things to be explained.

Now we'll use an interesting feature of SWR, called `mutate`.
Using `mutate` we can modify the local cache of comment list before we even refetch the updated list from Supabase server.

Let's discover the behaviour by just implementing it. Update the `onSubmit` function, and edit our add comment form.

```typescript:pages/index.tsx

...

  const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const newComment = {
      username: "hoonwee@email.com",
      payload: comment,
      reply_of: replyOf,
    };
    if (typeof commentList !== "undefined") {
      mutate("api/comments", [...commentList, newComment], false);
      const response = await addCommentRequest("api/comments", newComment);
      if (response[0].created_at) {
        mutate("api/comments");
        window.alert("Hooray!");
        setComment("")
      }
    }
  };

...

	<input
		onChange={onChange}
		value={comment}
		type="text"
		placeholder="Add a comment"
		className="p-2 border-b focus:border-b-gray-700 w-full outline-none"
	/>

```

We removed our old `await supabase...` and replaced it with someting else:

- We added two `mutate` functions, which will refetch comment list that has added a new comment. But why two?
  - The first one won't actually refetch the data.
    Instead it will **assume** that adding a comment has succeeded, and **pretend** that it refetched it by modifying the local cache of comment list.
  - Now the second one will actually refetch the data, and compare between data modified and data refetched.
    When it's equal, it does nothing. While there's any difference, it will rerender for the correct comment list.
- There's a `await addCommentRequest` function call in between two `mutate` functions. This will send a POST request to `comments.ts` API, and return the response for the request.
  - Once succeeded adding a comment, it will return an array with single comment item.
  - So if the response is an array, and the first element has `created_at` field, the request is confirmed to be successful
    so we'll use second `mutate` function to compare with modified cache, and initalize the comment form with `setComment` by setting an empty string.

Now with our powerful cache-modifying code, we can see updated comment list **without reloading** the page!

![Without reloading](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/obbi20kv1al45hp5rwl5.gif)

### Replacing "edit, delete comments"

Let's practice using `mutate` one more time, replacing old code for editing comment.
Add & Replace code like down below.

```typescript:pages/index.tsx

...

const editCommentRequest = (url: string, data: any) =>
  fetch(url, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  }).then((res) => res.json());

...

  const confirmEdit = async () => {
    const editData = {
      payload: editComment.payload,
      commentId: editComment.id,
    };
    if (typeof commentList !== "undefined") {
      mutate(
        "api/comments",
        commentList.map((comment) => {
          if (comment.id === editData.commentId) {
            return { ...comment, payload: editData.payload };
          }
        }),
        false
      );
      const response = await editCommentRequest("api/comments", editData);
      console.log(response);
      if (response[0].created_at) {
        mutate("api/comments");
        window.alert("Hooray!");
        setEditComment({ id: "", payload: "" });
      }
    }
  };

...

```

The flow is the same as we've done for `onSubmit` function.

- We first added a `editCommentRequest` fetcher function.
- We added two `mutate`, the _pretending_ one, and the _real one_ in `confirmEdit`.
- Before executing 2nd `mutate`, we check if our request has succeeded with `response[0].created_at`.
- Finally we reset the `editComment` state.

Let's do the same work for deleting comments.

```typescript:pages/index.tsx

...

const deleteCommentRequest = (url: string, id: string) =>
  fetch(`${url}?comment_id=${id}`, { method: "DELETE" }).then((res) => res.json());

...

  const confirmDelete = async (id: string) => {
    const ok = window.confirm("Delete comment?");
    if (ok && typeof commentList !== "undefined") {
      mutate(
        "api/comments",
        commentList.filter((comment) => comment.id !== id),
        false
      );
      const response = await deleteCommentRequest("api/comments", id);
      if (response[0].created_at) {
        mutate("api/comments");
        window.alert("Deleted Comment :)");
      }
    }
  };

...

```

No explaination needed! It's the same as we did for editing comment.

Try editing & deleting comment, and check if the comment list changes properly without reloading.

## And we are done!

Congratulations! We successfully built a comments section with feature of:

- CRUD(Create, Read, Update, Delete)ing the comments, with **Supabase** node library.
- Mutate UI without reloading with **SWR**
- Clean & understandable design, powered by **TailwindCSS** and **Hero Icons**.

Although our comment section is awesome, there are some improvements to be made (do it by yourself!):

- Replace browser's alert/confirm window to _toast UI_. It will look better.
- Implement _user login_, to make it usable in community service. You can make it from scratch, or...
  - Use Supabase's native user table
  - Use [Magic](https://magic.link/).
  - Use [NextAuth](https://next-auth.js.org/).
- Transform the replying system into _threads_.

![Toast UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m5j77ebgt3w4mskraqz1.gif)

And that's all for this series! Thank you so much for following up this far, and I hope to see you on my next blog post/series!

**Until then, happy coding!**