Pagination with React.js and Supabase

In this article, we learn how to paginate data with Supabase and React.js

·6 min read
Cover Image for Pagination with React.js and Supabase

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!


Stay informed with our latest resources for building a SaaS

Subscribe to our newsletter to receive updatesor

Read more about

Cover Image for Next.js 13: complete guide to Server Components and the App Directory

Next.js 13: complete guide to Server Components and the App Directory

·11 min read
A tutorial on how to use Next.js 13 with server components and the app directory.
Cover Image for How to sell code with Lemon Squeezy and Github

How to sell code with Lemon Squeezy and Github

·7 min read
Sell and monetize your code by giving private access to your Github repositories using Lemon Squeezy
Cover Image for Writing clean React

Writing clean React

·9 min read
Learn how to write clean React code using Typescript with this guide.
Cover Image for How to use MeiliSearch with React

How to use MeiliSearch with React

·12 min read
Learn how to use MeiliSearch in your React application with this guide. We will use Meiliseach to add a search engine for our blog posts
Cover Image for Setting environment variables in Remix

Setting environment variables in Remix

·3 min read
Learn how to set environment variables in Remix and how to ensure that they are available in the client-side code.
Cover Image for Programmatic Authentication with Supabase and Cypress

Programmatic Authentication with Supabase and Cypress

·3 min read
Testing code that requires users to be signed in can be tricky. In this post, we show you how to sign in programmatically with Supabase Authentication to improve the speed of your Cypress tests and increase their reliability.