Skip to main content

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 to paginate through lists of data and the Pagination component 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:


Anchor to Setup the paginated querySetup the paginated query

First, set up a GraphQL query to the Storefront API 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

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 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

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,
});
}

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

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

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>
);
}

Anchor to Complete pagination exampleComplete pagination example

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

app/route/products.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}
`;

Anchor to Automatically load pages on scrollAutomatically 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

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}
));
}

Was this page helpful?