Firestore: Data Fetching

Learn how to fetch data from Firestore in your React applications.

Before writing the Components, we want to tackle the task of fetching data from Firestore.

This involves a couple of steps:

  1. We need to define, on paper, what the data model of the Firestore collections looks like
  2. We need to update the Firestore rules so we can read and write data to the collection
  3. We need to write the hooks to fetch data from Firebase Firestore

Firestore Data Model

First, let's write the data model. But... what does it mean?

When thinking about the Firestore data model, we need to answer the following questions:

  1. Where does the data live?
  2. How is the data structured?
  3. How do we protect the data with security rules?

Ok, let's reply to these questions.

Where does the data live?

We can place tasks as a Firestore top-level collection. Because tasks belong to an organization, we can ensure that only users of that organization can read and write those tasks by adding a foreign key to each task named organizationId.

How is the data structured?

We can define a Task model at src/lib/tasks/types/task.ts:

src/lib/tasks/types/task.ts
export interface Task { name: string; description: string; organizationId: string; dueDate: string; done: boolean; }
How do we protect the data with security rules?

To write our Security Rules, we will update the file firestore.rules:

match /tasks/{taskId} { allow create: if userIsMemberByOrganizationId(incomingData().organizationId); allow read, update, delete: if userIsMemberByOrganizationId(existingData().organizationId); }

The function userIsMemberByOrganizationId is pre-defined in the Makerkit's template: basically, it will verify that the current user is a member of the organization with ID organizationId.

If you use VSCode, take a look at the extension toba.vsfire.

Data Fetching: React Hooks to read data from Firestore

Now that our security rules are updated, we can write Hooks to read data from Firestore.

I recommend writing your entities' hooks at src/lib/tasks/hooks. Let's start with a React Hook to fetch all the organization's tasks:

src/lib/tasks/hooks/use-fetch-tasks.ts
import { useFirestore, useFirestoreCollectionData } from 'reactfire'; import { collection, CollectionReference, query, where, } from 'firebase/firestore'; import { Task } from '~/lib/tasks/types/task'; function useFetchTasks(organizationId: string) { const firestore = useFirestore(); const tasksCollection = 'tasks'; const collectionRef = collection( firestore, tasksCollection ) as CollectionReference<WithId<Task>>; const path = `organizationId`; const operator = '=='; const constraint = where(path, operator, organizationId); const organizationsQuery = query(collectionRef, constraint); return useFirestoreCollectionData(organizationsQuery, { idField: 'id', }); } export default useFetchTasks;

Let's take a deep breath and slowly go through the above hook.

  1. The hook above uses useFirestoreCollectionData, a React hook from the library reactfire to fetch real-time collection data from Firestore
  2. We build a query that checks that the organizationId parameter matches the organizationId property of each task
  3. Finally, we add { idField: 'id' }. This special parameter decorates the data with the ID of the document. Thanks to the interface WithId, we add an id property to the Task interface returned from Firestore

Displaying Firestore data in Components

Now that we can fetch our data using the hook useFetchTasks, we can build a component that lists each Task.

To do so, let's create a new component named TasksListContainer:

components/tasks/TasksListContainer.tsx
import PageLoadingIndicator from '~/core/ui/PageLoadingIndicator'; import Alert from '~/core/ui/Alert'; import Heading from '~/core/ui/Heading'; import Button from '~/core/ui/Button'; import useFetchTasks from '~/lib/tasks/hooks/use-fetch-tasks'; import TasksList from '~/components/tasks/TasksList'; import { Task } from '~/lib/tasks/types/task'; const TasksContainer: React.FC<{ organizationId: string; }> = ({ organizationId }) => { const { status, data: tasks } = useFetchTasks(organizationId); if (status === `loading`) { return <PageLoadingIndicator>Loading Tasks...</PageLoadingIndicator>; } if (status === `error`) { return ( <Alert type={'error'}> Sorry, we encountered an error while fetching your tasks. </Alert> ); } if (tasks.length === 0) { return <TasksEmptyState />; } return ( <div className={'flex flex-col space-y-4'}> <div className={'mt-2 flex justify-end'}> <CreateTaskButton>New Task</CreateTaskButton> </div> <div className={'flex flex-col space-y-4'}> {tasks.map((task) => { return <TaskListItem task={task} key={task.id} />; })} </div> </div> ); }; function TasksEmptyState() { return ( <div className={ 'flex flex-col items-center justify-center space-y-4 h-full p-24' } > <div> <Heading type={5}>No tasks found</Heading> </div> <CreateTaskButton>Create your first Task</CreateTaskButton> </div> ); } function TasksList({ tasks }: React.PropsWithChildren<{ tasks: Task[] }>) { return ( <div className={'flex flex-col space-y-4'}> {tasks.map((task) => { return <TaskListItem task={task} key={task.id} />; })} </div> ); } export default TasksList; function CreateTaskButton(props: React.PropsWithChildren) { return <Button href={'/tasks/new'}>{props.children}</Button>; } export default TasksContainer;

The data above will automatically update when the tasks collection changes!


Subscribe to our Newsletter
Get the latest updates about React, Remix, Next.js, Firebase, Supabase and Tailwind CSS