Before writing the React Components, we want to tackle the task of fetching data from Supabase.
This involves a couple of steps:
- We need to define, on paper, what the data model of the Supabase tables looks like
- We need to update the SQL schema of the database to match the data model and write the requires RLS policies to secure the data
- We need to write the hooks to fetch data from Supabase
How is the data structured?
We can place tasks as its own Postgres table tasks
. Let's create an SQL
table that defines the following columns:
create table tasks (
id bigint generated always as identity primary key,
organization_id bigint not null references public.organizations,
name text not null,
done bool not null,
due_date timestamptz not null
);
Because tasks belong to an
organization, we need to ensure that only users of that organization can read
and write those tasks by adding a foreign key to each task
named
organization_id
.
Replicating the tasks table as a Type
We can define a Task
model at src/lib/tasks/types/task.ts
:
export interface Task {
id: number;
name: string;
organizationId: string;
dueDate: string;
done: boolean;
}
NB: this may not be necessary if you generate the types from the SQL schema. Up to you.
How do we protect the data with RLS?
We can add a RLS policy to the tasks
table to ensure that only users of the same organization can read and write tasks.
We know that an user belongs to an organization using the memberships
table.
The memberships
table has two foreign keys: user_id
and
organization_id
: we can use these foreign keys to ensure that only users
of the same organization can read and write tasks.
create policy "Tasks can be read by users of the organizations to which it belongs" on tasks
for all
using (exists (
select
1
from
memberships
where
user_id = auth.uid () and tasks.organization_id = memberships.organization_id));
Generating the types from the SQL schema
We can generate the types from the SQL schema using the supabase-js
library. To run this command, you need to ensure Supabase is running running
npm run supabase:start
.
You can do so by running the following command:
npm run typegen
The types will be generated at app/database-types.ts
. We will import the
types whenever we use the Supabase client.
For example:
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '../../database.types';
type Client = SupabaseClient<Database>;
Data Fetching: React Hooks to read data from Supabase Postgres
With our tables and RLS in place, we can finally start writing our queries using the Supabase Postgres client.
I recommend writing your entities' hooks at lib/tasks/hooks
. Let's start
with a React Hook
to fetch all the organization's tasks.
First, we want to write a function that is responsible for fetching the
tasks using the Supabase Postgres client. We can write this function at
lib/tasks/queries.ts
:
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '../../database.types';
import { TASKS_TABLE } from '~/lib/db-tables';
import Task from '~/lib/tasks/types/task';
type Client = SupabaseClient<Database>;
export function getTasks(client: Client, organizationId: number) {
return client
.from(TASKS_TABLE)
.select<string, Task>(
`
id,
name,
organization_id: organizationId,
dueDate: due_date,
done
`
)
.eq('organization_id', organizationId)
.throwOnError();
}
The query above will be fetching all the tasks of the organization with the
ID organizationId
.
As you can see, my convention (used throughout the boilerplate) is to write queries that inject the Supabase client as a parameter. This allows me to reuse these functions in both the browser and the server.
Additionally, I also like to write queries that throw errors. This allows me
to catch errors using try/catch blocks rather than using the error
property. This is a personal preference, feel free to use the error
property instead.
Writing the React Hook to fetch the tasks
Now that we have an isomorphic function to fetch the tasks, we can write a React Hook to fetch the tasks.
In the boilerplate, we use react-query
as a
wrapper around the Supabase client. This can help with coordinating
refetching, caching, and general state management using hooks when fetching
data.
We can write this hook at
lib/tasks/hooks/use-fetch-tasks.ts
:
import useSupabase from '~/core/hooks/use-supabase';
import { useQuery } from '@tanstack/react-query';
import { getTasks } from '~/lib/tasks/queries';
export function useFetchTasks(
organizationId: number
) {
const client = useSupabase();
const key = ['tasks', organizationId];
return useQuery(
key,
async () => {
return getTasks(client, organizationId).then((res) => res.data)
},
);
}
export default useFetchTasks;
Displaying Supabase Database 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 type Task from '~/lib/tasks/types/task';
const TasksContainer: React.FC<{
organizationId: string;
}> = ({ organizationId }) => {
const { data: tasks, error, isLoading } = useFetchTasks(organizationId);
if (isLoading) {
return <PageLoadingIndicator>Loading Tasks...</PageLoadingIndicator>;
}
if (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>
<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 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;
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.