While reactfire
does not provide hooks for writing data to Firestore, we can still make our own.
In fact, I encourage you to make a custom hook for each mutation.
Mutations
Creating a Task
In the example below, we write a custom hook to add a document to the tasks
collection:
import { useFirestore } from 'reactfire';
import { useCallback } from 'react';
import { addDoc, collection } from 'firebase/firestore';
import { Task } from '~/lib/tasks/types/task';
function useCreateTask() {
const firestore = useFirestore();
const tasksCollection = collection(firestore, `/tasks`);
return useCallback(
(task: Task) => {
return addDoc(tasksCollection, task);
},
[tasksCollection]
);
}
export default useCreateTask;
The hook above returns a callback. Let's take a quick look at its usage:
import useCreateTask from '~/lib/tasks/hooks/use-create-task';
function Component() {
const createTask = useCreateTask();
return <Form onCreate={task => createTask(task)} />
}
Updating a Task
Now, let's write a hook to update an existing task.
To update Firestore documents, we will import the function updateDoc
from Firestore.
import { useCallback } from 'react';
import { useFirestore } from 'reactfire';
import { doc, updateDoc } from 'firebase/firestore';
import { Task } from '~/lib/tasks/types/task';
function useUpdateTask(taskId: string) {
const firestore = useFirestore();
const tasksCollection = 'tasks';
const docRef = doc(firestore, tasksCollection, taskId);
return useCallback(
(task: Partial<Task>) => {
return updateDoc(docRef, task);
},
[docRef]
);
}
export default useUpdateTask;
Deleting a Task
Finally, we write a mutation to delete a task:
import { useFirestore } from 'reactfire';
import { deleteDoc, doc } from 'firebase/firestore';
import { useCallback } from 'react';
function useDeleteTask(taskId: string) {
const firestore = useFirestore();
const collection = `tasks`;
const task = doc(firestore, collection, taskId);
return useCallback(() => {
return deleteDoc(task);
}, [task]);
}
export default useDeleteTask;
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 from 'next/link';
import { TrashIcon } from '@heroicons/react/24/outline';
import { toast } from 'sonner';
import { formatDistance } from 'date-fns';
import { 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 useDeleteTask from '~/lib/tasks/hooks/use-delete-task';
import ConfirmDeleteTaskModal from '~/components/tasks/ConfirmDeleteTaskModal';
import useUpdateTask from '~/lib/tasks/hooks/use-update-task';
const TasksListItem: React.FC<{
task: WithId<Task>;
}> = ({ task }) => {
const getTimeAgo = useTimeAgo();
const deleteTask = useDeleteTask(task.id);
const updateTask = useUpdateTask(task.id);
const [isDeleting, setIsDeleting] = useState(false);
const onDelete = useCallback(() => {
const deleteTaskPromise = deleteTask();
return toast.promise(deleteTaskPromise, {
success: `Task deleted!`,
loading: `Deleting task...`,
error: `Ops, error! We could not delete task`,
});
}, [deleteTask]);
const onDoneChange = useCallback(
(done: boolean) => {
const promise = updateTask({ done });
return toast.promise(promise, {
success: `Task updated!`,
loading: `Updating task...`,
error: `Ops, error! We could not update task`,
});
},
[updateTask]
);
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'}
href={`/tasks/[id]`}
as={`/tasks/${task.id}`}
passHref
>
{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 TasksListItem;
function useTimeAgo() {
return useCallback((date: Date) => {
return formatDistance(date, new Date(), {
addSuffix: true,
});
}, []);
}