In the section above, we saw how to fetch data from the Supabase Postgres. Now, let's see how to write data to the Supabase database.
Creating a Task
First, we want to add a file named mutations.ts
at lib/tasks/
. Here, we
will add all the mutations that we will need to create, update, and delete
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,
});
}
Now, we can use the createTask
mutation in our useCreateTask
hook.
We will be using the useMutation
hook from react-query
to create our hook.
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;
And now, we could use this hook within a component. The below is a very simple example:
function Component() {
const createTaskMutation = useCreateTaskMutation();
return <MyForm onSubmit={task => createTaskMutation.trigger(task)} />
}
Updating a Task
Now, let's write a hook to update an existing task.
We will write another mutation function to update a task:
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 updateTask(
client: Client,
task: Partial<Task> & { id: number }
) {
return client
.from(TASKS_TABLE)
.update({
name: task.name,
done: task.done,
})
.match({
id: task.id,
})
.throwOnError();
}
And we can write a hook to use this mutation:
import useSWRMutation from 'swr/mutation';
import useSupabase from '~/core/hooks/use-supabase';
import type Task from '~/lib/tasks/types/task';
import { updateTask } from '~/lib/tasks/mutations';
type TaskPayload = Partial<Task> & { id: number };
function useUpdateTaskMutation() {
const client = useSupabase();
const key = ['tasks'];
return useSWRMutation(key, async (_, { arg: task }: { arg: TaskPayload }) => {
return updateTask(client, task);
});
}
export default useUpdateTaskMutation;
Deleting a Task
We will write another mutation function to delete a task:
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '../../database.types';
import { TASKS_TABLE } from '~/lib/db-tables';
type Client = SupabaseClient<Database>;
export function deleteTask(
client: Client,
taskId: number
) {
return client.from(TASKS_TABLE).delete().match({
id: taskId
}).throwOnError();
}
Finally, we write a mutation to delete a task:
import useSWRMutation from 'swr/mutation';
import useSupabase from '~/core/hooks/use-supabase';
import { deleteTask } from '~/lib/tasks/mutations';
function useDeleteTaskMutation() {
const client = useSupabase();
const taskId = ['tasks'];
return useSWRMutation(
taskId,
async (_, { arg: taskId }: { arg: number }) => {
return deleteTask(client, taskId);
});
}
export default useDeleteTaskMutation;
Using the Modal component for confirming actions
The Modal
component is excellent for asking for confirmation before performing a destructive action. In our case, we want the users to confirm before deleting a task.
The ConfirmDeleteTaskModal
is defined below using the core Modal
component:
import Modal from '~/core/ui/Modal';
import Button from '~/core/ui/Button';
const ConfirmDeleteTaskModal: React.FC<{
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
task: string;
onConfirm: () => void;
}> = ({ isOpen, setIsOpen, onConfirm, task }) => {
return (
<Modal heading={`Deleting Task`} isOpen={isOpen} setIsOpen={setIsOpen}>
<div className={'flex flex-col space-y-4'}>
<p>
You are about to delete the task <b>{task}</b>
</p>
<p>Do you want to continue?</p>
<Button block color={'danger'} onClick={onConfirm}>
Yep, delete task
</Button>
</div>
</Modal>
);
};
export default ConfirmDeleteTaskModal;
Wrapping all up: listing each Task Item
Now that we have defined the mutations we can perform on each task item, we can wrap it up.
The component below represents a task
item and allows users to mark it as done
or not done
- or delete them.
'use client';
import type { MouseEventHandler } from 'react';
import { useCallback, useState } from 'react';
import { TrashIcon } from '@heroicons/react/24/outline';
import toaster from 'react-hot-toast';
import { formatDistance } from 'date-fns';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import type Task from '~/lib/tasks/types/task';
import Heading from '~/core/ui/Heading';
import IconButton from '~/core/ui/IconButton';
import { Tooltip, TooltipContent, TooltipTrigger } from '~/core/ui/Tooltip';
import useDeleteTaskMutation from '~/lib/tasks/hooks/use-delete-task';
import useUpdateTaskMutation from '~/lib/tasks/hooks/use-update-task';
import ConfirmDeleteTaskModal
from '~/app/(app)/tasks/components/ConfirmDeleteTaskModal';
const TaskListItem: React.FC<{
task: Task;
}> = ({ task }) => {
const getTimeAgo = useTimeAgo();
const deleteTaskMutation = useDeleteTaskMutation();
const updateTaskMutation = useUpdateTaskMutation();
const router = useRouter();
const [isDeleting, setIsDeleting] = useState(false);
const onDelete = useCallback(async () => {
const deleteTaskPromise = deleteTaskMutation
.trigger(task.id);
await toaster.promise(deleteTaskPromise, {
success: `Task deleted!`,
loading: `Deleting task...`,
error: `Ops, error! We could not delete task`,
});
await router.refresh();
setIsDeleting(false);
}, [deleteTaskMutation, router, task.id]);
const onDoneChange = useCallback(
(done: boolean) => {
const promise = updateTaskMutation.trigger({ done, id: task.id });
return toaster.promise(promise, {
success: `Task updated!`,
loading: `Updating task...`,
error: `Ops, error! We could not update task`,
});
},
[task.id, updateTaskMutation]
);
const onDeleteRequested: MouseEventHandler = useCallback((e) => {
e.stopPropagation();
setIsDeleting(true);
}, []);
return (
<>
<div
className={'rounded border p-4 transition-colors dark:border-black-400'}
>
<div className={'flex items-center space-x-4'}>
<div>
<Tooltip>
<TooltipTrigger>
<input
className={'Toggle cursor-pointer'}
type="checkbox"
defaultChecked={task.done}
onChange={(e) => {
return onDoneChange(e.currentTarget.checked);
}}
/>
</TooltipTrigger>
<TooltipContent>
{task.done ? `Mark as not done` : `Mark as done`}
</TooltipContent>
</Tooltip>
</div>
<div className={'flex flex-1 flex-col space-y-0.5'}>
<Heading type={5}>
<Link className={'hover:underline'} href={`/tasks/${task.id}`}>
{task.name}
</Link>
</Heading>
<div>
<p className={'text-xs text-gray-400 dark:text-gray-500'}>
Due {getTimeAgo(new Date(task.dueDate))}
</p>
</div>
</div>
<div className={'flex justify-end'}>
<Tooltip>
<TooltipTrigger asChild>
<IconButton onClick={onDeleteRequested}>
<TrashIcon className={'h-5 text-red-500'} />
</IconButton>
</TooltipTrigger>
<TooltipContent>Delete Task</TooltipContent>
</Tooltip>
</div>
</div>
</div>
<ConfirmDeleteTaskModal
isOpen={isDeleting}
setIsOpen={setIsDeleting}
task={task.name}
onConfirm={onDelete}
/>
</>
);
};
export default TaskListItem;
function useTimeAgo() {
return useCallback((date: Date) => {
return formatDistance(date, new Date(), {
addSuffix: true,
});
}, []);
}