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!