Firestore: Data Writing
Learn how to write data to Firestore in your React applications.
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, }); }, []);}