Supabase & Next.js part 2
BCiriak Avatar
by BCiriakJUN 25, 2024 | 10 min read

Supabase Insert and Revalidate To-dos - Part 2

This is the second part of the Supabase Database with Next.js tutorial, learn to insert to-dos to the Supabase Database using Next.js and React Server Components.


To follow along, here is the GIT repository with the 01-todo-app-part-1 branch that has all the changes from the first part.

In this part, we will start with inserting to-dos to our Supabase Database, the creation piece of the CRUD actions.

Next.js and Server Components

Since we are using Next.js 14+, we are also using React Server Components. If you are not familiar with this new React concept, read this great article by Josh Comeau - Making Sense of React Server Components.

To sum it up VERY simply, React components are by default Server Components, and they represent "static" parts of the application while Client Components are handling user interactions with the web.

Add To-do Client Component

That is why we can't just add a button with its onClick event to our page.tsx, which is a Server Component. What we can do, is create a Client Component that will have that button and text input and add that component to our page.tsx. Let's do that. Create a new folder in the root of our app called components and inside that folder create AddTodo.tsx file.

./components/AddTodo.tsx
'use client'

import { useState } from 'react'

export default function AddTodo() {
  const [todo, setTodo] = useState('')

  return (
    <div>
      <input
        type='text'
        className='border'
        value={todo}
        onChange={(e) => setTodo(e.target.value)}
      />
      <button onClick={() => {}} disabled={todo === ''}>
        Add
      </button>
    </div>
  )
}

This is a basic component with one distinction, the first line defines it as a Client Component with use client. Other than that, pretty boring, for now onClick does nothing.

We have a couple of ways to add the querying functionality to this component. We want to query the Supabase Database, which is asynchronous operation, but we never want to make our Client Component async as we will get this message from Next.js - No Async Client Components.

Supabase Insert Query

So the simple way to do this, is to add an asynchronous function that will query the database, in the page.tsx file (Server component). But if we would create this function inside the component, we would bump into another problem. We can't pass functions as parameters from Server Components to Client Components.

Simple solution for now is, to export async function directly from the page.tsx file. Don't worry, we will do a little refactor once we get it working.

./app/page.tsx
import { Database } from '@/types/supabase'
import { createClient } from '@supabase/supabase-js'
import AddTodo from '@/components/AddTodo'

const supabase = createClient<Database>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

export default async function Home() {
  const { data, error } = await supabase.from('Todos').select('*')

  if (error) {
    console.error('error', error)
  }

  return (
    <div>
      <h1>Home</h1>
      <AddTodo />
      {data && (
        <ul>
          {data.map((todo) => (
            <li key={todo.id}>{todo.description}</li>
          ))}
        </ul>
      )}
    </div>
  )
}

export const addTodo = async (todo: string) => {
  const { error } = await supabase
    .from('Todos')
    .insert({ description: todo })

  if (error) {
    console.error('error', error)
  }
}

We have added our AddTodo component on the line 20 and at the bottom is the function to insert to-do into our Supabase Database. Again, the syntax is very intuitive.

Insert data to Supabase Database

Now we just need to adjust AddTodo component to call the insert function from page.tsx. We can also improve user experience by clearing the to-do state after we call the addTodo function. Here is the final version of AddTodo component:

./components/AddTodo.tsx
'use client'

import { useState } from 'react'
import { addTodo } from '@/app/page'

export default function AddTodo() {
  const [todo, setTodo] = useState('')

  const handleAddTodo = () => {
    addTodo(todo)
    setTodo('')
  }

  return (
    <div>
      <input
        type='text'
        className='border'
        value={todo}
        onChange={(e) => setTodo(e.target.value)}
      />
      <button onClick={handleAddTodo} disabled={todo === ''}>
        Add
      </button>
    </div>
  )
}

We imported the addTodo function and added a little handleAddTodo function that will call the insert to-do function and clear the todo state. There is a little problem if we try this in our browser. After adding the to-do, we don't see it in our list of to-dos.

Revalidate Supabase data

The problem is, we don't have the latest data from our database, the data is not fresh. To solve this, we can use Next.js function that will refresh our data.

./app/page.tsx
export const addTodo = async (todo: string) => {
  const { error } = await supabase.from('Todos').insert({ description: todo })

  if (error) {
    console.error('error', error)
  }

  revalidatePath('/')
}

Since we are inserting the data on our home page, we can simply revalidate this path. With this change, we will see our new to-do in the list right after we click on the Add button.

Refactor insert into separate file

With these changes, we now can do a small refactor and move all of our querying logic to a separate file, where we will add also functions to update and delete our to-dos.

Let's create a new folder called actions inside the root and also create file called todos.tsx where will all of our Supabase querying logic reside. With that done, let's move both of our functions to this file:

./actions/todos.tsx
'use server'

import { revalidatePath } from 'next/cache'
import { createClient } from '@supabase/supabase-js'
import { Database } from '@/types/supabase'

const supabase = createClient<Database>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

export const getTodos = async () => {
  const { data, error } = await supabase.from('todos').select('*')

  if (error) {
    console.error('error', error)
  }

  return { data, error }
}

export const addTodo = async (todo: string) => {
  const { data, error } = await supabase.from('todos').insert({
    description: todo,
  })

  if (error) {
    console.error('error', error)
  }

  revalidatePath('/')
}

Now we can update our page.tsx file as follows:

./app/page.tsx
'use server'

import { getTodos } from '@/actions/todos'
import AddTodo from '@/components/AddTodo'

export default async function Home() {
  const { data } = await getTodos()

  return (
    <div>
      <h1>Home</h1>
      <AddTodo />
      {data && (
        <ul>
          {data.map((todo) => (
            <li key={todo.id}>{todo.description}</li>
          ))}
        </ul>
      )}
    </div>
  )
}

Nice and simple. We also need to update the import of our addTodo function in AddTodo.tsx component to the new function in todos.tsx file.

./app/components/AddTodo.tsx
import { addTodo } from '@/actions/todos'

Everything should be working as before and we can start thinking about adding functionality to remove to-dos. To make it simple, we will just add Delete button next to each to-do and when we click on it, we simply remove the item.

Todo List and Todo Item components

Now we have good opportunity to continue with the refactor and to keep our components small and clean, we can add two new components, TodoList and TodoItem. This will keep our page.tsx clean and concise. Let's start with the TodoList, create it in the components folder and insert this code:

./app/components/TodoList.tsx
import { Tables } from '@/types/supabase'

type Props = {
  todos: Array<Tables<'Todos'>>
}

export default function TodoList({ todos }: Props) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.description}</li>
      ))}
    </ul>
  )
}

One thing to mention, is the Tables import, which is how we can type our properties based on specific table, very neat.

Let's also update our Homepage, so that we use this component.

./app/page.tsx
'use server'

import { getTodos } from '@/actions/todos'
import AddTodo from '@/components/AddTodo'
import TodoList from '@/components/TodoList'

export default async function Home() {
  const { data } = await getTodos()

  return (
    <div>
      <h1>Home</h1>
      <AddTodo />
      {data && <TodoList todos={data} />}
    </div>
  )
}

We have extracted querying functionality to its own file, kind of like a service, and also added couple of components to show our to-dos. In the next part, we will add the last two functions, Delete and Update. Let's see how in the final part of this tutorial.

Part 3 - Supabase Update and Delete To-dos

Keep learning and see you in the next one!

Connect with me on social media ✨

Join my newsletter, to receive JavaScript, TypeScript, React.js and more news, tips and other goodies right into your mail box 📥. You can unsubscribe at any time.

©2024 JSTopics. All rights reserved.