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
294 lines
8.2 KiB
Plaintext
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>
|
|
``` |