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
This commit is contained in:
committed by
GitHub
parent
4912e402a3
commit
7ebff31475
294
docs/development/writing-data-to-database.mdoc
Normal file
294
docs/development/writing-data-to-database.mdoc
Normal file
@@ -0,0 +1,294 @@
|
||||
---
|
||||
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>
|
||||
```
|
||||
Reference in New Issue
Block a user