Pagination with React.js and Supabase

Discover the best practices for paginating data using Supabase and React.js using the Supabase Postgres client

Paginating data is a common task in web development. In this article, we learn how to paginate data with Supabase and React.js, and then render the data in a table using React Table, likely the best table library for React.js.

Prerequisites

We assume you have a basic understanding of React.js and Supabase, and a bare-bones codebase to work with. We also assume you have a Supabase project set up and a table with some data in it.

In our example, we will be fetching data from a PostgreSQL table called organizations, then proceed to paginate the data and render it in a table.

The pagination is driven a page query parameter, set using the utilities provided by React Table. Fetching the data can be done in various ways depending on which framework you will be using, such as Remix or Next.js. For simplicity, we will fetch the data simply using the supabase-js library.

Fetching the data

We will be using the supabase-js library to fetch the data from the organizations table.

Our getOrganizations function will take two parameters, the client and the params object. The client is the Supabase client, and the params object contains the from and to index of the data to fetch:

export async function getOrganizations(
client: Client,
params: {
from: number;
to: number;
}
) {
return await client
.from('organizations')
.select('*')
.range(params.from, params.to);
}

Assuming we're fetching this data in a React component, we can then call the getOrganizations function:

const ITEMS_PER_PAGE = 10;
function OrganizationsDataProvider(
props: React.PropsWithChildren<{
page: number;
}>
) {
const [state, setState] = useState({
loading: true,
error: undefined,
data: undefined,
});
// Get the Supabase client
const client = useSupabase();
useEffect(() => {
async function fetchOrganizations() {
try {
setState({
loading: true,
error: undefined,
data: undefined,
});
const from = props.page * ITEMS_PER_PAGE;
const to = from + ITEMS_PER_PAGE;
const { data, error } = await getOrganizations(client, {
from,
to,
});
if (error) {
throw error;
}
setState({
loading: false,
error: undefined,
data,
});
} catch (error) {
setState({
loading: false,
error,
data: undefined,
});
}
}
fetchOrganizations();
}, [client, page]);
return (
<OrganizationsTable organizations={organizations} />
);

As you can see in the code above, we receive the page parameter from the parent component, and then use it to calculate the from and to index of the data to fetch. We then call the getOrganizations function, and pass the client and the from and to index as parameters.

The page parameter will ideally be passed from the URL, so we can simply update the page by changing the URL.

For example, if we're using Next.js, we can use the useRouter hook to get the page parameter from the URL:

import { useRouter } from 'next/router';
function OrganizationsPage() {
const router = useRouter();
const page = router.query.page || 0;
return (
<OrganizationsDataProvider page={page} />
);
}

Paginating the data

Now that we have the data, we define the component OrganizationsTable to render the table to paginate the data.

function OrganizationsTable(
props: React.PropsWithChildren<{
organizations: Organization[];
}>
) {
const columns = useMemo(() => {
return [
{
id: 'id',
header: 'Id',
accessorKey: 'id',
cell: (ctx) => ctx.getValue(),
},
{
id: 'name',
header: 'Name',
accessorKey: 'name',
cell: (ctx) => ctx.getValue(),
},
];
}, []);
return (
<Table
data={organizations}
columns={columns}
onPaginationChange={({ pageIndex }) => {
// update the search params with page=pageIndex
// this will trigger a re-render of the component
// and fetch the data for the new page
}}
/>
);
}

As you can see, the onPaginationChange callback is called when the user changes the page. We can then update the page query parameter in the URL, which will trigger a re-render of the component and fetch the data for the new page.

Depending on the router you're using, you can use the useRouter hook from next/router or useLocation hook from react-router-dom, or setSearchParams from remix.

Building the Table component

We've built the Table component using React Table, which is a great library for building tables in React.js.

If you're looking for the full source code, here is where you can read it: Reusable Table component with React Table.

Alternative approaches with SSR

The alternative approach, which is likely the best one, is to load the data from the server, and then pass it to the client. This also makes it easier to restrict the maximum amount of items to fetch.

Example: fetching data with Next.js

For example, if we're using Next.js, we can use the getServerSideProps function to fetch the data from the server, and then pass it to the client:

const ITEMS_PER_PAGE = 20;
export async function getServerSideProps(ctx) {
const page = ctx.query.page || 0;
const client = getSupabaseServerClient(); // you will have to implement this
const from = page * ITEMS_PER_PAGE;
const to = from + ITEMS_PER_PAGE;
const { data: organizations, error } = await getOrganizations(client, {
from,
to,
});
if (error) {
return ctx.res.status(500).end();
}
return {
props: {
organizations
},
};
}

Then, you could fully skip the OrganizationsDataProvider component, and simply pass the organizations prop to the OrganizationsTable component:

function OrganizationsPage(props) {
return (
<OrganizationsTable organizations={props.organizations} />
);
}

Example: fetching data with Remix

If you're using Remix, you can use the useLoaderData hook to fetch the data from the loader function, and then pass it to the client:

const ITEMS_PER_PAGE = 20;
export async function loader({ request }: LoaderArgs) {
const page = new URL(request.url).searchParams.get('page') || 0;
const client = getSupabaseServerClient(); // you will have to implement this
const from = page * ITEMS_PER_PAGE;
const to = from + ITEMS_PER_PAGE;
const { data: organizations, error } = await getOrganizations(client, {
from,
to,
});
if (error) {
return new Response(`Could not load data`, { status: 500 });
}
return json({ organizations });
}
export default function OrganizationsPage() {
const data = useLoaderData<typeof loader>();
return (
<OrganizationsTable organizations={data.organizations} />
);
}

Example: fetching data with Next.js 13 Server Components

If you're using Next.js 13 with Server Components, you can use the use hook to fetch the data from the Server Component and pass it down to the client components:

function OrganizationsPage() {
const { data, error } = use(
getOrganizations(client, {
from,
to,
})
);
if (error) {
return <div>Could not load data</div>;
}
return (
<OrganizationsTable organizations={data} />
);
}

Conclusion

In this article, we've seen how to fetch paginated data from a Supabase database, and then paginate the data using React Table.

We've also seen how to fetch the data from the server, and then pass it to the client using Next.js, Remix, and Next.js 13 Server Components.

If you have any questions, feel free to contact me in our Discord Server!