This documentation is for a legacy version of Next.js and Supabase. For the latest version, please visit the Next.js and Supabase V2 documentation

Writing data to Database | Next.js Supabase SaaS Kit

Learn how to write, update and delete data using the Supabase Database in Next.js Supabase SaaS project

Similarly to reading data, we may want to create a custom hook also when we write, update or delete data to our Supabase database.

Assuming we have an entity called tasks, and we want to create a hook to create a new Task, we can do the following.

  1. First, we create a new hook at lib/tasks/hooks/use-create-task.ts
  2. We add the tasks table name to lib/db-tables.ts
  3. We create a mutations.ts file within lib/tasks
export const TASKS_TABLE = `tasks`;

In our mutations file, we will add all the mutations we want to perform on the tasks table. In this case, we will add a createTask mutation.

import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '../../database.types';
import type Task from '~/lib/tasks/types/task';
import { TASKS_TABLE } from '~/lib/db-tables';
type Client = SupabaseClient<Database>;
export function createTask(client: Client, task: Omit<Task, 'id'>) {
return client.from(TASKS_TABLE).insert({
name: task.name,
organization_id: task.organizationId,
due_date: task.dueDate,
done: task.done,
});
}

Calling mutation using a Server Action

The best way to call a mutation is to use a Next.js Server Action. This way, we can use the revalidatePath function to revalidate the page that we want to revalidate after the mutation is completed.

'use server';
import { revalidatePath } from 'next/cache';
import { createTask } from '~/lib/tasks/mutations';
import type Task from '~/lib/tasks/types/task';
import { withSession } from '~/core/generic/actions-utils';
import getSupabaseServerActionClient from '~/core/supabase/action-client';
type CreateTaskParams = {
task: Omit<Task, 'id'>;
csrfToken: string;
};
export const createTaskAction = withSession(
async (params: CreateTaskParams) => {
const client = getSupabaseServerActionClient();
await createTask(client, params.task);
revalidatePath('/dashboard/[organization]/tasks');
}
);

Then, we call the action from a form:

'use client';
import type { FormEventHandler } from 'react';
import { useCallback, useTransition } from 'react';
import { toast } from 'sonner';
import { useRouter } from 'next/navigation';
import TextField from '~/core/ui/TextField';
import Button from '~/core/ui/Button';
import If from '~/core/ui/If';
import useCurrentOrganization from '~/lib/organizations/hooks/use-current-organization';
import { createTaskAction } from '~/lib/tasks/actions';
import useCsrfToken from '~/core/hooks/use-csrf-token';
const CreateTaskForm = () => {
const [isMutating, startTransition] = useTransition();
const organization = useCurrentOrganization();
const organizationId = organization?.id as number;
const csrfToken = useCsrfToken();
const router = useRouter();
const onCreateTask: FormEventHandler<HTMLFormElement> = useCallback(
async (event) => {
event.preventDefault();
const target = event.currentTarget;
const data = new FormData(target);
const name = data.get('name') as string;
const dueDate = (data.get('dueDate') as string) || getDefaultDueDate();
if (name.trim().length < 3) {
toast.error('Task name must be at least 3 characters long');
return;
}
const task = {
organizationId,
name,
dueDate,
done: false,
};
startTransition(async () => {
await createTaskAction({ task, csrfToken });
router.push('../tasks');
});
},
[csrfToken, organizationId, router]
);
return (
<form onSubmit={onCreateTask}>
<div className={'flex flex-col space-y-2'}>
<TextField.Label>
Name
<TextField.Input
required
name={'name'}
placeholder={'ex. Launch on IndieHackers'}
/>
<TextField.Hint>Hint: whatever you do, ship!</TextField.Hint>
</TextField.Label>
<TextField.Label>
Due date
<TextField.Input name={'dueDate'} type={'date'} />
</TextField.Label>
<div
className={
'flex flex-col space-y-2 md:flex-row md:space-x-2 md:space-y-0'
}
>
<Button loading={isMutating}>
<If condition={isMutating} fallback={<>Create Task</>}>
Creating Task...
</If>
</Button>
<Button color={'transparent'} href={'../tasks'}>
Go back
</Button>
</div>
</div>
</form>
);
};
function getDefaultDueDate() {
const date = new Date();
date.setDate(date.getDate() + 1);
date.setHours(23, 59, 59);
return date.toDateString();
}
export default CreateTaskForm;

Calling mutation using SWR

Alternatively, we can use the createTask mutation in our useCreateTask hook.

We will be using the useSWRMutation hook from swr to create our hook.

lib/tasks/hooks/use-create-task.ts
import useSWRMutation from 'swr/mutation';
import { useRouter } from 'next/navigation';
import useSupabase from '~/core/hooks/use-supabase';
import { createTask } from '~/lib/tasks/mutations';
import type Task from '~/lib/tasks/types/task';
function useCreateTaskMutation() {
const client = useSupabase();
const router = useRouter();
const key = 'tasks';
return useSWRMutation(key, async (_, { arg: task }: { arg: Omit<Task, 'id'> }) => {
return createTask(client, task);
}, {
onSuccess: () => router.refresh()
});
}
export default useCreateTaskMutation;

Let's take a look at a complete example of a form that makes a request using the hook above:

components/tasks/CreateTaskForm.tsx
import { useRouter } from 'next/router';
import type { FormEventHandler } from 'react';
import { useCallback } from 'react';
import { toast } from 'sonner';
import TextField from '~/core/ui/TextField';
import Button from '~/core/ui/Button';
import useCreateTaskMutation from '~/lib/tasks/hooks/use-create-task';
import useCurrentOrganization from '~/lib/organizations/hooks/use-current-organization';
const CreateTaskForm = () => {
const createTaskMutation = useCreateTaskMutation();
const router = useRouter();
const organization = useCurrentOrganization();
const organizationId = organization?.id as number;
const onCreateTask: FormEventHandler<HTMLFormElement> = useCallback(
async (event) => {
event.preventDefault();
const target = event.currentTarget;
const data = new FormData(target);
const name = data.get('name') as string;
const dueDate = (data.get('dueDate') as string) || getDefaultDueDate();
if (name.trim().length < 3) {
toast.error('Task name must be at least 3 characters long');
return;
}
const task = {
organizationId,
name,
dueDate,
done: false,
};
// create task
await createTaskMutation.trigger(task);
// redirect to /tasks
return router.push(`/tasks`);
},
[router, createTaskMutation, organizationId]
);
return (
<form onSubmit={onCreateTask}>
<div>
<TextField.Label>
Name
<TextField.Input
required
name={'name'}
placeholder={'ex. Launch on IndieHackers'}
/>
<TextField.Hint>Hint: whatever you do, ship!</TextField.Hint>
</TextField.Label>
<TextField.Label>
Due date
<TextField.Input name={'dueDate'} type={'date'} />
</TextField.Label>
<div>
<Button>Create Task</Button>
</div>
</div>
</form>
);
};
export default CreateTaskForm;
function getDefaultDueDate() {
const date = new Date();
date.setDate(date.getDate() + 1);
date.setHours(23, 59, 59);
return date.toDateString();
}