Data Loader SDK

Fetching data from Supabase directly from declarative React Components using the Data Loader SDK - a lightweight wrapper around the Supabase JS Client.

The Data Loader SDK is a lightweight open-source wrapper around the Supabase JS Client. The source code can be found here.

The Data Loader SDK provides a declarative way to fetch data from Supabase directly from React components - from both React Client and Server components (RSC). This means they play well both using RSC and SSR/SPA - so you can choose the approach that best fits your needs.

You can also use the utility anywhere you want - e.g. in a React Hook, in a React Component, in a Next.js API Route, a Server Action, etc.

Why?

These components help us load data from Supabase in a declarative way - so we can focus on building our application instead of worrying about how to fetch data from Supabase.

It's type-safe thanks to the types exported by the Supabase JS Client - so you can be sure that the data you're fetching is the data you're expecting.

The Data Loader SDK

Installation

The Data Loader SDK is provided as an independent (and open-source) package - @makerkit/data-loader-supabase-nextjs. It is not included in the SaaS Kit by default - so (for the time being) you need to install it separately.

To install it, run the following command:

npm i @makerkit/data-loader-supabase-nextjs

To import the Data Loader SDK, you can use multiple approaches.

  1. Server Components - i.e. to be used in RSCs
  2. Client Components - i.e. to be used in SPA/SSR
  3. React Hook - i.e. to be used in React Client Components
  4. Directly using the fetchDataFromSupabase function. Useful for server-side code - e.g. in a Route Handler, or Server Action

Server Components

With React Server Components - i.e. to be used in RSCs:

import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';

These stream compiled JSX directly to the browser and are ideal when you want to fetch the data only on the server and never on the client.

Client Components

With React Client Components - i.e. to be used in SPA/SSR:

import { ClientDataLoader } from '@makerkit/data-loader-supabase-nextjs';

These are ideal when you want to fetch the data in both the server and the client.

useSupabaseQuery React Hook

Alternatively, you can use a React Hook - which can only be used in React Client Components:

import { useSupabaseQuery } from '@makerkit/data-loader-supabase-nextjs'; import useSupabase from '~/core/supabase/use-supabase'; function OrganizationsTable() { const client = useSupabase(); const { data, isLoading, error } = useSupabaseQuery({ client, table: 'organizations', select: '*', }); if (isLoading) { return <span>Loading...</span>; } if (error) { return <span>Error: {error.message}</span>; } return ( <DataTable data={data} columns={[ { header: 'ID', accessorKey: 'id', }, { header: 'Name', accessorKey: 'name', } ]} /> ); }

Directly from the "useSupabaseQuery" function

Underneath the hood, the ServerDataLoader and ClientDataLoader components use the fetchDataFromSupabase function - which can be used directly to fetch data from Supabase.

The fetchDataFromSupabase function is exported from the @makerkit/data-loader-supabase-core package - which is a dependency of the @makerkit/data-loader-supabase-nextjs package.

You can use this anywhere you want - e.g. in a React Hook, in a React Component, in a Next.js API Route, etc. This is why you will need to pass the appropriate Supabase Client to the function.

import { NextResponse } from "next/server"; import { fetchDataFromSupabase } from '@makerkit/data-loader-supabase-core'; import getSupabaseRouteHandlerClient from '~/core/supabase/route-handler-client'; export async function GET() { const client = getSupabaseRouteHandlerClient(); const { data, count, pageSize, pageCount } = await fetchDataFromSupabase({ client, table: 'organizations', select: '*', where: { name: { textSearch: `'makerkit'`, } } }); return NextResponse.json({ data, count, pageSize, pageCount, }); }

If you were to use it in a Server Action, you would need to pass the appropriate Supabase Client to the function.

'use server'; import { fetchDataFromSupabase } from '@makerkit/data-loader-supabase-core'; import getSupabaseServerActionClient from '~/core/supabase/server-action-client'; export async function serverAction() { const client = getSupabaseServerActionClient(); const { data, count, pageSize, pageCount } = await fetchDataFromSupabase({ client, table: 'organizations', select: '*', where: { name: { textSearch: `'makerkit'`, } } }); return { data, count, pageSize, pageCount, }; }

Usage

The Data Loader SDK provides two components:

  • ServerDataLoader - for React Server Components
  • ClientDataLoader - for React Client Components

The two works exactly the same way - but you need to choose the one that best fits your needs. The only difference is in what they return.

  1. ServerDataLoader returns directly the data fetched from Supabase - since the loading state is not something you need to worry about as it's handled by the server and the data is already available when the component is rendered.
  2. ClientDataLoader uses SWR under the hood and exposes the properties data, loading and error - since the loading state is something you need to worry about as it's handled by the client and the data is not available when the component is rendered.

API

Both components accept the following properties:

Required:

  • client - the Supabase Client to use to fetch data. This is required.
  • table - the table to fetch data from. This is autocompleted thanks to the Typescript types exported by the Supabase JS Client. This is required.

Optional:

  • select - the columns to fetch listed as an array. You can also use first-level joins - e.g. ['id', 'name', 'organization_id.name']. The * wildcard is also supported - as in '*' (not an array). In this case, all the columns will be fetched.
  • where - the where clause to use to filter the data. This will change based on the table you're fetching data from.
  • sort - the order clause to use to order the data. This will change based on the table you're fetching data from.
  • page - the page to use to paginate the data.
  • limit - the limit clause to use to limit the data - i.e. the page size.
  • single - whether to return a single object or an array of objects. This is useful when you're fetching a single object - e.g. a user by its id. By default, just like the Supabase JS Client, it returns an array of objects. Unlike the Supabase JS Client, it will not throw an error when 0 or many objects are returned.
  • count - the count type to use to count the data. You can use either exact or estimated. By default, it uses exact.

ServerDataLoader

Let's see how to use the ServerDataLoader component.

We will build a simple table that lists all the organizations the user can read.

import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs'; import getSupabaseServerComponentClient from '~/core/supabase/server-component-client'; import DataTable from '~/core/ui/DataTable'; const OrganizationsTable = () => { const client = getSupabaseServerComponentClient(); return ( <ServerDataLoader client={client} table="organizations" select={["id", "name"]} > {({ data, count, pageSize, pageCount }) => { return ( <DataTable data={data} count={count} pageSize={pageSize} pageCount={pageCount} columns={[ { header: 'ID', accessorKey: 'id', }, { header: 'Name', accessorKey: 'name', } ]} /> ); }} </ServerDataLoader> ); };

We used the following properties:

  1. table - the table to fetch data from. In this case, organizations.
  2. select - the columns to fetch. In this case, ["id", "name"] - i.e. the id and name columns of the organizations table.

The ServerDataLoader component returns the following properties:

  1. data - the data fetched from Supabase. In this case, an array of objects with all the columns of the organizations table.
  2. count - the total number of rows in the table. In this case, the total number of rows in the organizations table.
  3. pageSize - the page size used to paginate the data. In this case, the default page size.
  4. pageCount - the total number of pages used to paginate the data. In this case, the total number of pages used to paginate the organizations table.

ClientDataLoader

Let's see how to use the ClientDataLoader component.

We will build a simple table that lists all the organizations the user can read. It works much the same way as the ServerDataLoader component - but it returns the following properties:

  1. result:
  2. data - the data fetched from Supabase. In this case, an array of objects with all the columns of the organizations table.
  3. count - the total number of rows in the table. In this case, the total number of rows in the organizations table.
  4. pageSize - the page size used to paginate the data. In this case, the default page size.
  5. pageCount - the total number of pages used to paginate the data. In this case, the total number of pages used to paginate the organizations table.
  6. isLoading - whether the data is loading or not.
  7. error - the error, if any.
  8. onPageChange - the function to call when the page changes. This is useful when you want to use the page property to paginate the data.
import { ClientDataLoader } from '@makerkit/data-loader-supabase-nextjs'; import useSupabase from '~/core/supabase/use-supabase'; interface SearchParams { page: string; } const OrganizationsTable = ({ searchParams }: { searchParams: SearchParams }) => { const client = useSupabase(); const page = Number(searchParams.page) || 1; return ( <ClientDataLoader client={client} table="organizations" page={page} // the page to fetch select="*" // all the columns - can be omitted limit={10} // retrieve 10 organizations per page > {({ result, isLoading }) => { const { data, count, pageSize, pageCount } = result; if (isLoading) { return <span>Loading...</span>; } return ( <DataTable data={data} count={count} pageSize={pageSize} pageCount={pageCount} columns={[ { header: 'ID', accessorKey: 'id', }, { header: 'Name', accessorKey: 'name', } ]} /> ); }} </ClientDataLoader> ); };

Filters

Let's see how to use the where property. We can use nearly all the supported operators by the Supabase JS Client.

Below, we pass where to the ClientDataLoader component to filter the organizations by their name. We use the textSearch operator to search for the word supabase in the name column.

import { ClientDataLoader } from '@makerkit/data-loader-supabase-nextjs'; <ClientDataLoader client={client} // the Supabase Client table="organizations" select="*" where={{ name: { textSearch: `'supabase'`, } }} />

We can also use other operators - e.g. in to filter the organizations by their id - i.e. to fetch only the organizations with the ids 1, 2 and 3.

import { ClientDataLoader } from '@makerkit/data-loader-supabase-nextjs'; <ClientDataLoader client={client} // the Supabase Client table="organizations" select="*" where={{ id: { in: [1, 2, 3], } }} />

Alternatively, we can use the eq operator to filter the organizations by their id - i.e. to fetch only the organizations with the id 1.

import { ClientDataLoader } from '@makerkit/data-loader-supabase-nextjs'; <ClientDataLoader client={client} table="organizations" select="*" where={{ id: { eq: 1, } }} />

Select

Let's see how to use the select property.

Below, we pass select to the ClientDataLoader component to select only the id and name columns.

import { ClientDataLoader } from '@makerkit/data-loader-supabase-nextjs'; <ClientDataLoader client={client} table="organizations" select={['id', 'name']} />

Let's assume we want to fetch the organization name of the tasks table, which has a organization_id column that references the organizations table. We can use first-level joins to fetch the organization name.

import { ClientDataLoader } from '@makerkit/data-loader-supabase-nextjs'; <ClientDataLoader client={client} table="tasks" select={['id', 'name', 'organization_id.name']} />

The data returned will be an array of objects with the following structure:

[ { "id": 1, "name": "Task 1", "organization_id": { "name": "Organization 1" } }, { "id": 2, "name": "Task 2", "organization_id": { "name": "Organization 2" } } ]

The '*' wildcard is also supported - as in '*' (not an array). In this case, all the columns will be fetched.

import { ClientDataLoader } from '@makerkit/data-loader-supabase-nextjs'; <ClientDataLoader client={client} table="organizations" select="*" />

You don't have to provide it - as it's the default value. The following is equivalent to the previous example.

import { ClientDataLoader } from '@makerkit/data-loader-supabase-nextjs'; <ClientDataLoader client={client} table="organizations" />

Single Object

Let's see how to use the single property. We can use this property to unwrap the data returned by the ServerDataLoader component - since it returns an array of objects by default.

Below, we pass single to the ClientDataLoader component to fetch a single organization by its id.

import { ClientDataLoader } from '@makerkit/data-loader-supabase-nextjs'; <ClientDataLoader table="organizations" select={["id", "name"]} single where={{ id: { eq: 1, } }} />

The data returned will be an object with the following structure:

{ "id": 1, "name": "Organization 1" }

Bear in mind, a value can be undefined when not found - so you need to handle this case.

Camel Case

Let's see how to use the camelCase property. We can use this property to convert the column names to camel case - since normally your Postgres column names are snake case.

Below, we pass camelCase to the ServerDataLoader component to convert the column names to camel case.

Assuming the organizations table has a organization_name column, the data returned will be an array of objects with the following structure:

[ { "id": 1, "organizationName": "Organization 1" }, { "id": 2, "organizationName": "Organization 2" } ]

Check out the following example:

import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs'; function Component() { return ( <ServerDataLoader client={client} table="organizations" select={["id", "organization_name"]} camelCase > {({ data }) => { return ( <div> <span>{data.id}</span> <span>{data.organizationName}</span> </div> ); }} </ServerDataLoader> ); }

The TypeScript types are also updated accordingly - so you can be sure that the data you're fetching is the data you're expecting.


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