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

Learn how to write, update and delete data using the Supabase Database in Makerkit

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();
}