Learn how to write data to the Supabase database
In this page we learn how to write data to the Supabase database in your Next.js app
In this page, we will learn how to write data to the Supabase database in your Next.js app.
Writing a Server Action to Add a Task
Server Actions are defined by adding use server
at the top of the function or file. When we define a function as a Server Action, it will be executed on the server-side.
This is useful for various reasons:
- By using Server Actions, we can revalidate data fetched through Server Components
- We can execute server side code just by calling the function from the client side
In this example, we will write a Server Action to add a task to the database.
Defining a Schema for the Task
We use Zod to validate the data that is passed to the Server Action. This ensures that the data is in the correct format before it is written to the database.
The convention in Makerkit is to define the schema in a separate file and import it where needed. We use the convention file.schema.ts
to define the schema.
import { z } from 'zod';export const WriteTaskSchema = z.object({ title: z.string().min(1), description: z.string().nullable(),});
Writing the Server Action to Add a Task
In this example, we write a Server Action to add a task to the database. We use the revalidatePath
function to revalidate the /home
page after the task is added.
'use server';import { revalidatePath } from 'next/cache';import { redirect } from 'next/navigation';import { z } from 'zod';import { getLogger } from '@kit/shared/logger';import { requireUser } from '@kit/supabase/require-user';import { getSupabaseServerClient } from '@kit/supabase/server-client';import { WriteTaskSchema } from '~/(dashboard)/home/(user)/_lib/schema/write-task.schema';export async function addTaskAction(params: z.infer<typeof WriteTaskSchema>) { 'use server'; const task = WriteTaskSchema.parse(params); const logger = await getLogger(); const client = getSupabaseServerClient(); const auth = await requireUser(client); if (!auth.data) { redirect(auth.redirectTo); } logger.info(task, `Adding task...`); const { data, error } = await client .from('tasks') .insert({ ...task, account_id: auth.data.id }); if (error) { logger.error(error, `Failed to add task`); throw new Error(`Failed to add task`); } logger.info(data, `Task added successfully`); revalidatePath('/home', 'page'); return null;}
Let's focus on this bit for a second:
const { data, error } = await client .from('tasks') .insert({ ...task, account_id: auth.data.id });
Do you see the account_id
field? This is a foreign key that links the task to the user who created it. This is a common pattern in database design.
Now that we have written the Server Action to add a task, we can call this function from the client side. But we need a form, which we define in the next section.
Creating a Form to Add a Task
We create a form to add a task. The form is a React component that accepts a SubmitButton
prop and an onSubmit
prop.
import { zodResolver } from '@hookform/resolvers/zod';import { useForm } from 'react-hook-form';import { z } from 'zod';import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,} from '@kit/ui/form';import { Input } from '@kit/ui/input';import { Textarea } from '@kit/ui/textarea';import { Trans } from '@kit/ui/trans';import { WriteTaskSchema } from '../_lib/schema/write-task.schema';export function TaskForm(props: { task?: z.infer<typeof WriteTaskSchema>; onSubmit: (task: z.infer<typeof WriteTaskSchema>) => void; SubmitButton: React.ComponentType;}) { const form = useForm({ resolver: zodResolver(WriteTaskSchema), defaultValues: props.task, }); return ( <Form {...form}> <form className={'flex flex-col space-y-4'} onSubmit={form.handleSubmit(props.onSubmit)} > <FormField render={(item) => { return ( <FormItem> <FormLabel> <Trans i18nKey={'tasks:taskTitle'} /> </FormLabel> <FormControl> <Input required {...item.field} /> </FormControl> <FormDescription> <Trans i18nKey={'tasks:taskTitleDescription'} /> </FormDescription> <FormMessage /> </FormItem> ); }} name={'title'} /> <FormField render={(item) => { return ( <FormItem> <FormLabel> <Trans i18nKey={'tasks:taskDescription'} /> </FormLabel> <FormControl> <Textarea {...item.field} /> </FormControl> <FormDescription> <Trans i18nKey={'tasks:taskDescriptionDescription'} /> </FormDescription> <FormMessage /> </FormItem> ); }} name={'description'} /> <props.SubmitButton /> </form> </Form> );}
Using a Dialog component to display the form
We use the Dialog component from the @kit/ui/dialog
package to display the form in a dialog. The dialog is opened when the user clicks on a button.
'use client';import { useState, useTransition } from 'react';import { PlusCircle } from 'lucide-react';import { Button } from '@kit/ui/button';import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger,} from '@kit/ui/dialog';import { Trans } from '@kit/ui/trans';import { TaskForm } from '../_components/task-form';import { addTaskAction } from '../_lib/server/server-actions';export function NewTaskDialog() { const [pending, startTransition] = useTransition(); const [isOpen, setIsOpen] = useState(false); return ( <Dialog open={isOpen} onOpenChange={setIsOpen}> <DialogTrigger asChild> <Button> <PlusCircle className={'mr-1 h-4'} /> <span> <Trans i18nKey={'tasks:addNewTask'} /> </span> </Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle> <Trans i18nKey={'tasks:addNewTask'} /> </DialogTitle> <DialogDescription> <Trans i18nKey={'tasks:addNewTaskDescription'} /> </DialogDescription> </DialogHeader> <TaskForm SubmitButton={() => ( <Button> {pending ? ( <Trans i18nKey={'tasks:addingTask'} /> ) : ( <Trans i18nKey={'tasks:addTask'} /> )} </Button> )} onSubmit={(data) => { startTransition(async () => { await addTaskAction(data); setIsOpen(false); }); }} /> </DialogContent> </Dialog> );}
We can now import NewTaskDialog
in the /home
page and display the dialog when the user clicks on a button.
Let's go back to the home page and add the component right next to the input filter:
<div className={'flex items-center justify-between'}> <div> <Heading level={4}> <Trans i18nKey={'tasks:tasksTabLabel'} defaults={'Tasks'} /> </Heading> </div> <div className={'flex items-center space-x-2'}> <form className={'w-full'}> <Input name={'query'} defaultValue={query} className={'w-full lg:w-[18rem]'} placeholder={'Search tasks'} /> </form> <NewTaskDialog /> </div></div>