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.
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 { useMutation } from '@tanstack/react-query';
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();
return useMutation(async (task: Omit<Task, 'id'>) => {
return createTask(client, task);
});
}
export default useCreateTaskMutation;
And now, we could use this hook within a component:
function Component() {
const createTaskMutation = useCreateTaskMutation();
return <MyForm onSubmit={task => createTaskMutation.mutateAsync(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:
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 { useMutation } from '@tanstack/react-query';
import useSupabase from '~/core/hooks/use-supabase';
import type Task from '~/lib/tasks/types/task';
import { updateTask } from '~/lib/tasks/mutations';
function useUpdateTaskMutation() {
const client = useSupabase();
return useMutation(async (task: Partial<Task> & { id: number }) => {
return updateTask(client, task);
});
}
export default useUpdateTaskMutation;
Deleting a Task
We will write another mutation function to delete a task:
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 { useMutation } from '@tanstack/react-query';
import useSupabase from '~/core/hooks/use-supabase';
import { deleteTask } from '~/lib/tasks/mutations';
function useDeleteTaskMutation() {
const client = useSupabase();
return useMutation(async (taskId: 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.
import { useCallback, useState } from 'react';
import { Link, useNavigate } from '@remix-run/react';
import { TrashIcon } from '@heroicons/react/24/outline';
import toaster from 'react-hot-toast';
import { formatDistance } from 'date-fns';
import type Task from '~/lib/tasks/types/task';
import Heading from '~/core/ui/Heading';
import IconButton from '~/core/ui/IconButton';
import Tooltip from '~/core/ui/Tooltip';
import useDeleteTaskMutation from '~/lib/tasks/hooks/use-delete-task';
import ConfirmDeleteTaskModal from '~/components/tasks/ConfirmDeleteTaskModal';
import useUpdateTaskMutation from '~/lib/tasks/hooks/use-update-task';
const TaskListItem: React.FC<{
task: Task;
}> = ({ task }) => {
const getTimeAgo = useTimeAgo();
const deleteTaskMutation = useDeleteTaskMutation();
const updateTaskMutation = useUpdateTaskMutation();
const navigate = useNavigate();
const [isDeleting, setIsDeleting] = useState(false);
const onDelete = useCallback(() => {
const deleteTaskPromise = deleteTaskMutation.mutateAsync(task.id).then(() => {
navigate('.')
});
return toaster.promise(deleteTaskPromise, {
success: `Task deleted!`,
loading: `Deleting task...`,
error: `Ops, error! We could not delete task`,
});
}, [deleteTaskMutation, navigate, task.id]);
const onDoneChange = useCallback(
(done: boolean) => {
const promise = updateTaskMutation.mutateAsync({ 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]
);
return (
<>
<div
className={'rounded border p-4 transition-colors dark:border-black-400'}
>
<div className={'flex items-center space-x-4'}>
<div>
<Tooltip content={task.done ? `Mark as not done` : `Mark as done`}>
<input
className={'Toggle cursor-pointer'}
type="checkbox"
defaultChecked={task.done}
onChange={(e) => {
return onDoneChange(e.currentTarget.checked);
}}
/>
</Tooltip>
</div>
<div className={'flex flex-1 flex-col space-y-0.5'}>
<Heading type={5}>
<Link className={'hover:underline'} to={`/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 content={`Delete Task`}>
<IconButton
onClick={(e) => {
e.stopPropagation();
setIsDeleting(true);
}}
>
<TrashIcon className={'h-5 text-red-500'} />
</IconButton>
</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,
});
}, []);
}