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

Subscribe to our Newsletter
Get the latest updates about React, Remix, Next.js, Firebase, Supabase and Tailwind CSS