Files
myeasycms-v2/docs/development/writing-data-to-database.mdoc
Giancarlo Buomprisco 7ebff31475 Next.js Supabase V3 (#463)
Version 3 of the kit:
- Radix UI replaced with Base UI (using the Shadcn UI patterns)
- next-intl replaces react-i18next
- enhanceAction deprecated; usage moved to next-safe-action
- main layout now wrapped with [locale] path segment
- Teams only mode
- Layout updates
- Zod v4
- Next.js 16.2
- Typescript 6
- All other dependencies updated
- Removed deprecated Edge CSRF
- Dynamic Github Action runner
2026-03-24 13:40:38 +08:00

294 lines
8.2 KiB
Plaintext

---
status: "published"
label: "Writing data to Database"
order: 5
title: "Learn how to write data to the Supabase database in your Next.js app"
description: "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.
{% sequence title="How to write data to the Supabase database" description="In this page we learn how to write data to the Supabase database in your Next.js app" %}
[Writing a Server Action to Add a Task](#writing-a-server-action-to-add-a-task)
[Defining a Schema for the Task](#defining-a-schema-for-the-task)
[Writing the Server Action to Add a Task](#writing-the-server-action-to-add-a-task)
[Creating a Form to Add a Task](#creating-a-form-to-add-a-task)
[Using a Dialog component to display the form](#using-a-dialog-component-to-display-the-form)
{% /sequence %}
## 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:
1. By using Server Actions, we can revalidate data fetched through Server Components
2. 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.
```tsx
import * as 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.
```tsx
'use server';
import { revalidatePath } from 'next/cache';
import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { authActionClient } from '@kit/next/safe-action';
import { WriteTaskSchema } from '~/home/(user)/_lib/schema/write-task.schema';
export const addTaskAction = authActionClient
.inputSchema(WriteTaskSchema)
.action(async ({ parsedInput: task, ctx: { user } }) => {
const logger = await getLogger();
const client = getSupabaseServerClient();
logger.info(task, `Adding task...`);
const { data, error } = await client
.from('tasks')
.insert({ ...task, account_id: user.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');
});
```
Let's focus on this bit for a second:
```tsx
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.
```tsx
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import * as 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.
```tsx
'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:
```tsx {18}
<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>
```