Before writing the Components, we want to tackle the task of fetching data from Firestore.
This involves a couple of steps:
- We need to define, on paper, what the data model of the Firestore collections looks like
- We need to update the Firestore rules so we can read and write data to the collection
- 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:
- Where does the data live?
- How is the data structured?
- 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
:
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:
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.
- The hook above uses
useFirestoreCollectionData
, aReact hook
from the libraryreactfire
to fetch real-time collection data from Firestore - We build a query that checks that the
organizationId
parameter matches theorganizationId
property of each task - Finally, we add
{ idField: 'id' }
. This special parameter decorates the data with the ID of the document. Thanks to the interfaceWithId
, we add anid
property to theTask
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
:
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';
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={'flex justify-end'}>
<CreateTaskButton>New Task</CreateTaskButton>
</div>
<TasksList tasks={tasks} />
</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 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!
Hold on, we're not done yet!
Since the component "TaskListItem" is pretty complex and requires more files, we will define it separately. You will find it below after we explain the mutation hooks that the component will use.