---
title: Pagination
description: Learning about rendering paginated data from the Storefront API.
source_url:
  html: >-
    https://shopify.dev/docs/storefronts/headless/legacy/hydrogen/data-fetching/pagination
  md: >-
    https://shopify.dev/docs/storefronts/headless/legacy/hydrogen/data-fetching/pagination.md
api_name: hydrogen
---

Legacy Hydrogen

These pages refer to Hydrogen versions 2026-04 and earlier. For versions 2026-10 and later, refer to the latest [Hydrogen docs](https://shopify.dev/docs/storefronts/hydrogen). If you're not using the latest version of Hydrogen, then we recommend that you migrate your storefront. To learn more, refer to [Migrate to the latest version of Hydrogen](https://shopify.dev/docs/storefronts/hydrogen/migrate).

# Pagination

The Storefront API limits how many items can be queried at once. This encourages better app performance by only querying what's immediately necessary to render the page.

However, sometimes you might have long lists of products, collections, or orders. Rather than rendering every item in the list, for better performance you should only render one page at a time. The [Storefront API uses cursors](https://shopify.dev/docs/api/usage/pagination-graphql) to paginate through lists of data and the [`Pagination` component](https://shopify.dev/docs/api/hydrogen/2026-04/components/pagination) enables you to render those pages.

It's important to maintain the pagination state in the URL for the following reasons:

* Users can navigate to a product and return back to the same scrolled position in a list.

* The list state is shareable by URL.

* Search engine crawlers are also able to index the pages when the pagination state is stored in the URL,

  To set up pagination inside your app, do the following tasks:

***

## Setup the paginated query

First, set up a GraphQL query to the [Storefront API](https://shopify.dev/docs/api/storefront) to return paginated content. A query needs to have the arguments `first`, `last`, `startCursor`, and `endCursor`.

The query response needs to include `pageInfo` with `hasPreviousPage`, `hasNextPage`, `startCursor`, and `endCursor` passed to it.

## app/route/products.jsx

```js
const PRODUCT_CARD_FRAGMENT = `#graphql
  fragment ProductCard on Product {
    id
    title
    publishedAt
    handle
    vendor
    variants(first: 1) {
      nodes {
        id
        image {
          url
          altText
          width
          height
        }
        price {
          amount
          currencyCode
        }
        compareAtPrice {
          amount
          currencyCode
        }
        selectedOptions {
          name
          value
        }
        product {
          handle
          title
        }
      }
    }
  }
`;


const ALL_PRODUCTS_QUERY = `#graphql
  query AllProducts(
    $first: Int
    $last: Int
    $startCursor: String
    $endCursor: String
  ) {
    products(first: $first, last: $last, before: $startCursor, after: $endCursor) {
      nodes {
        ...ProductCard
      }
      pageInfo {
        hasPreviousPage
        hasNextPage
        startCursor
        endCursor
      }
    }
  }
  ${PRODUCT_CARD_FRAGMENT}
`;
```

Hydrogen provides the utility [`getPaginationVariables`](https://shopify.dev/docs/api/hydrogen/2026-04/utilities/getpaginationvariables) to help calculate these variables from URL parameters. We recommend using the utility to pass the variables to the query within your loader:

## app/route/products.jsx

```js
import {getPaginationVariables} from '@shopify/hydrogen';
import {json} from '@shopify/remix-oxygen';


export async function loader({context, request}) {
  const variables = getPaginationVariables(request, {
    pageBy: 4,
  });


  const {products} = await context.storefront.query(ALL_PRODUCTS_QUERY, {
    variables,
  });


  return json({
    products,
  });
}
```

***

## Render the `Pagination` component

Pass the entire query connection to the `Pagination` component. The component provides a `render` prop with all the nodes in the list. Map the nodes by product ID and render them.

## app/route/products.jsx

```jsx
import { Pagination } from "@shopify/hydrogen";
import { useLoaderData, Link } from "@remix-run/react";


export default function () {
  const { products } = useLoaderData();


  return (
    <Pagination connection={products}>
      {({ nodes }) => {
        return nodes.map((product) => (
          <link key={product.id} to={product.id} />
            {product.title}
          
        ));
      }}
    </Pagination>
  );
}
```

The `Pagination` component's render prop provides convenience links to either load more or previous product pages from nodes:

## app/route/products.jsx

```jsx
import { Pagination } from "@shopify/hydrogen";
import { useLoaderData, Link } from "@remix-run/react";


export default function () {
  const { products } = useLoaderData();


  return (
    <Pagination connection={products}>
      {({ nodes, NextLink, PreviousLink, isLoading  }) => (
        <>
          <PreviousLink>
            {isLoading ? "Loading..." : "Load previous products"}
          </PreviousLink>
          {nodes.map((product) => (
            <link key={product.id} to={product.id} />
              {product.title}
            
          ))}
          <NextLink>{isLoading ? "Loading..." : "Load next products"}</NextLink>
        
      )}
    </Pagination>
  );
}
```

***

## Complete pagination example

The following is a complete example of data fetching using pagination:

## app/route/products.jsx

```jsx
import {getPaginationVariables, Pagination} from '@shopify/hydrogen';
import {useLoaderData, Link} from '@remix-run/react';
import {json} from '@shopify/remix-oxygen';


export async function loader({context, request}) {
  const variables = getPaginationVariables(request, {
    pageBy: 4,
  });


  const {products} = await context.storefront.query(ALL_PRODUCTS_QUERY, {
    variables,
  });


  return json({
    products,
  });
}


export default function () {
  const {products} = useLoaderData();


  return (
    <Pagination connection={products}>
      {({nodes, NextLink, PreviousLink, isLoading}) => (
        <>
          <PreviousLink>
            {isLoading ? 'Loading...' : 'Load previous products'}
          </PreviousLink>
          {nodes.map((product) => (
            <link key={product.id} to={product.id} />
              {product.title}
            
          ))}
          <NextLink>{isLoading ? 'Loading...' : 'Load next products'}</NextLink>
        
      )}
    </Pagination>
  );
}


const PRODUCT_CARD_FRAGMENT = `#graphql
  fragment ProductCard on Product {
    id
    title
    publishedAt
    handle
    vendor
    variants(first: 1) {
      nodes {
        id
        image {
          url
          altText
          width
          height
        }
        price {
          amount
          currencyCode
        }
        compareAtPrice {
          amount
          currencyCode
        }
        selectedOptions {
          name
          value
        }
        product {
          handle
          title
        }
      }
    }
  }
`;


const ALL_PRODUCTS_QUERY = `#graphql
  query AllProducts(
    $first: Int
    $last: Int
    $startCursor: String
    $endCursor: String
  ) {
    products(first: $first, last: $last, before: $startCursor, after: $endCursor) {
      nodes {
        ...ProductCard
      }
      pageInfo {
        hasPreviousPage
        hasNextPage
        startCursor
        endCursor
      }
    }
  }
  ${PRODUCT_CARD_FRAGMENT}
`;
```

***

## Automatically load pages on scroll

We can change the implementation to support loading subsequent pages on scroll. Add the dependency `react-intersection-observer` and use the following example:

## app/route/products.jsx

```jsx
import { Pagination } from "@shopify/hydrogen";
import { useEffect } from "react";
import { useLoaderData, useNavigate } from "@remix-run/react";
import { useInView } from "react-intersection-observer";


export default function () {
  const { products } = useLoaderData();
  const { ref, inView, entry } = useInView();


  return (
    <Pagination connection={products}>
      {({ nodes, NextLink, hasNextPage, nextPageUrl, state }) => (
        <>
          <ProductsLoadedOnScroll
            nodes={nodes}
            inView={inView}
            hasNextPage={hasNextPage}
            nextPageUrl={nextPageUrl}
            state={state}
          />
          <NextLink ref={ref}>Load more</NextLink>
        
      )}
    </Pagination>
  );
}


function ProductsLoadedOnScroll({ nodes, inView, hasNextPage, nextPageUrl, state }) {
  const navigate = useNavigate();


  useEffect(() => {
    if (inView && hasNextPage) {
      navigate(nextPageUrl, {
        replace: true,
        preventScrollReset: true,
        state,
      });
    }
  }, [inView, navigate, state, nextPageUrl, hasNextPage]);


  return nodes.map((product) => (
    <link key={product.id} to={product.id} />
      {product.title}
    
  ));
}
```

***
