Announcing the Data Loader SDK for Supabase

We're excited to announce the Data Loader SDK for Supabase. It's a declarative, type-safe set of utilities to load data into your Supabase database that you can use in your Next.js or Remix apps.

I am super excited to announce the Data Loader SDK for Supabase, a new open source project that I have been working on for the past few weeks.

The Data Loader SDK is a declarative, type-safe set of utilities to load data from your Supabase database.

It's an external package that you can use in any Next.js or Remix project, and it's built on top of the Supabase JS Client. It's not limited to Makerkit's SaaS Kits.

Why?

Generally speaking - to remove boilerplate and make it easier to load data from your Supabase database. The SDK is built on top of the Supabase JS Client and uses the Supabase JS Client under the hood.

Installation

You can use the Data Loader SDK in any Next.js or Remix project. Yes - it's not limited to Makerkit's SaaS Kits.

To install it for Next.js, run the following command:

npm install @makerkit/data-loader-supabase-next.js

or if you use Remix:

npm install @makerkit/data-loader-supabase-remix

Usage

To use the Data Loader SDK, you have a variety of options.

  1. JSX: you can use it as a component, importing the components ServerDataLoader and ClientDataLoader from the SDK. The ServerDataLoader component can be used for Server Components (RSC), while the ClientDataLoader component will load data in Client Components. The Remix version only provides the ClientDataLoader component, as Remix doesn't support Server Components.
  2. Hooks: you can use the useSupabaseQuery to load data in your components. The Next.js uses SWR under the hood, while the Remix version uses TanStack's useQuery hook.
  3. Directly: you can use the fetchDataFromSupabase function from the core library to load data directly where you need it: for example, in API Routes, Server Actions, Remix Loaders/Actions, a Middleware, and so on.

Examples

Loading Supabase Data from a Server Component

In the example below, we want to pull data from a Supabase Database from within a Next.js Server Component. The data will be entirely fetched and rendered on the server.

Here's what we want to do:

  1. Import the ServerDataLoader component from the Data Loader SDK.
  2. Import the getSupabaseServerComponentClient function to load the Supabase Client to be used in the Server Component. Ideally, you have provided the Database interface - so that the Loader can infer the types of the data that you are loading.
  3. Pass the client prop to the ServerDataLoader component.
  4. Pass the table prop to the ServerDataLoader component. In our case, we want to load data from the organizations table.
  5. Define the fields that you want to load from the table using the select prop. In our case, we want to load the id and name fields.
  6. Pass the fetched data to the data prop of the DataTable component - which displays the data in a table.
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>
);
};

DX Goodies:

  1. Type Safety: the select prop is type-safe. If you try to select a field that doesn't exist in the table, you will get a TypeScript error.
  2. Intellisense: the select prop provides intellisense for the fields that you can select from the table. This depends on the client property: you must pass the Database types to the Supabase Client to get intellisense.
  3. Server-Side Rendering: the data is fetched and rendered on the server.
  4. Pagination: the ServerDataLoader component provides pagination out of the box. You can use the count, pageSize, and pageCount properties to display pagination controls. In Makerkit's DataTable component, the pagination will automatically handled for you.

As you can see in the below image, the IDE will provide intellisense for the fields that you can select from the table.

Intellisense for the select prop

Loading Supabase Data from a Client Component

In the example below, we want to pull data from a Supabase Database from within a Next.js Client Component. The data will be fetched on the client.

Generally speaking, the two components are very similar. The main difference is that we use the ClientDataLoader component we need to handle the loading state of the component - since the data is fetched on the client and is asynchronously loaded.

Below is an example of how you can use the ClientDataLoader component to load data from a Supabase Database from within a Next.js Server Component. The Server component passes the page query parameter to the ClientDataLoader, which is used to load the page of data that we want to display.

import useSupabase from '~/core/supabase/use-supabase';
import { ClientDataLoader } from '@makerkit/data-loader-supabase-nextjs';
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>
);
};

Loading Supabase Data from a Hook

In the example below, we want to pull data from a Supabase Database from within a Next.js Client Component using a React Hook `useSupabaseQ

Let's define a React Hook that we can use to load data from the organizations table.

import useSupabase from '~/core/supabase/use-supabase';
import { useSupabaseQuery } from '@makerkit/data-loader-supabase-nextjs';
export function useFetchOrganization() {
const client = useSupabase();
return useSupabaseQuery({
client,
table: 'organizations',
select: ['id', 'name'],
});
}

We can then use the useFetchOrganization hook in our components to load the data.

import { useFetchOrganization } from '~/hooks/use-fetch-organization';
const OrganizationsTable = () => {
const { result, isLoading } = useFetchOrganization();
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',
},
]}
/>
);
};

Loading Supabase Data Directly

Finally, you can also fetch data from anywhere by using the fetchDataFromSupabase function from the core library @makerkit/data-loader-supabase-core.

In the example below, we fetch the data from a Next.js API Route. We also pass the textSearch operator to the name field, which will perform a full-text search on the name field.

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',
where: {
name: {
textSearch: `'makerkit'`,
},
},
});
return NextResponse.json({
data,
count,
pageSize,
pageCount,
});
}

Fetching a single Object from Supabase

To fetch a single item, instead of a list of items, you can use the single prop. This will return a single object instead of an array of objects.

import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';
import getSupabaseServerComponentClient from '~/core/supabase/server-component-client';
const Organization = () => {
const client = getSupabaseServerComponentClient();
return (
<ServerDataLoader
client={client}
table="organizations"
select={['id', 'name']}
single
>
{({ data }) => {
return (
<div>
<h1>{data.name}</h1>
</div>
);
}}
</ServerDataLoader>
);
};

The data prop will be a single object instead of an array of objects.

Converting the data to camelCase

As it's commonly done in Postgres, the column names are in snake_case. If you want to convert the column names to camelCase (as it is more commonly done in Javascript), you can use the camelCase prop.

import { ServerDataLoader } from '@makerkit/data-loader-supabase-nextjs';
import getSupabaseServerComponentClient from '~/core/supabase/server-component-client';
const Task = () => {
const client = getSupabaseServerComponentClient();
return (
<ServerDataLoader
client={client}
table="tasks"
select={['name', 'created_at', 'updated_at']}
single
camelCase
>
{({ data }) => {
// data will be in camelCase
// {
// id: '...',
// createdAt: '...',
// updatedAt: '...',
// }
return (
<div>
<h1>{data.name} - {data.createdAt}</h1>
</div>
);
}}
</ServerDataLoader>
);
};

Passing the SWR or React Query options

When fetching data using the ClientDataLoader or the useSupabaseQuery hook, you can pass the config prop to the component. These props will be passed to the SWR or React Query hooks under the hood.

import { ClientDataLoader } from '@makerkit/data-loader-supabase-nextjs';
import useSupabase from '~/core/supabase/use-supabase';
const OrganizationsTable = () => {
const client = useSupabase();
return (
<ClientDataLoader
client={client}
table="organizations"
select={['id', 'name']}
config={{
refreshWhenOffline: false,
revalidateOnMount: false,
}}
>
{({ data }) => {
return (
<div>
<h1>{data.name}</h1>
</div>
);
}}
</ClientDataLoader>
);
};

You can do the same using the useSupabaseQuery hook.

import { useSupabaseQuery } from '@makerkit/data-loader-supabase-nextjs';
import useSupabase from '~/core/supabase/use-supabase';
function useFetchOrganization() {
const client = useSupabase();
return useSupabaseQuery({
client,
table: 'organizations',
select: ['id', 'name'],
config: {
refreshWhenOffline: false,
revalidateOnMount: false,
},
});
}

This allows you to customize the behavior of the SWR or React Query hooks - for example, you can disable the revalidateOnMount option to prevent the data from being fetched again when the component is mounted.

Conclusion

I hope you enjoyed this announcement. I am super excited about this new project, and I hope you will find it useful.

You can use it today in your Next.js or Remix projects. It's not limited to Makerkit's SaaS Kits - so feel free to use it in any project.

If you have any questions, feel free to reach out to me on our Discord server.

Happy coding!