Learn how to write, update and delete data using the Supabase Database in NextKit
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.
First, we create a new hook at lib/tasks/hooks/use-create-task.ts
We add the tasks
table name to lib/db-tables.ts
We create a mutations.ts
file within lib/tasks
ts 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.
ts 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.
ts 'use server' ;
import { revalidatePath } from 'next/cache' ;
import { createTask } from '~/lib/tasks/mutations' ;
import type Task from '~/lib/tasks/types/task' ;
import { withCsrfCheck, withSession } from '~/core/generic/actions-utils' ;
import getSupabaseServerActionClient from '@supabase/action-client' ;
type CreateTaskParams = {
task : Omit < Task , 'id' >;
csrfToken : string ;
};
export const createTaskAction = withSession (
withCsrfCheck ( async ( params : CreateTaskParams ) => {
const client = getSupabaseServerActionClient ();
await createTask (client, params.task);
revalidatePath ( '/dashboard/[organization]/tasks' );
})
);
Then, we call the action from a form:
ts 'use client' ;
import type { FormEventHandler } from 'react' ;
import { useCallback, useTransition } from 'react' ;
import { toast } from 'sonner' ;
import { useRouter } from 'next/navigation' ;
import TextField from '@/components/ui/TextField' ;
import Button from '@/components/ui/Button' ;
import If from '@/components/ui/If' ;
import useCurrentOrganization from '~/lib/organizations/hooks/use-current-organization' ;
import { createTaskAction } from '~/lib/tasks/actions' ;
import useCsrfToken from '@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 ts import useSWRMutation from 'swr/mutation' ;
import { useRouter } from 'next/navigation' ;
import useSupabase from '@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 ts import { useRouter } from 'next/router' ;
import type { FormEventHandler } from 'react' ;
import { useCallback } from 'react' ;
import { toast } from 'sonner' ;
import TextField from '@/components/ui/TextField' ;
import Button from '@/components/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 ();
}