How to Create Infinite Scroll Using Shopify Storefront API and React JS (Step-by-Step Guide)

Introduction: Infinite scroll in a Shopify storefront using React js
When we talk about infinite scroll in a Shopify storefront using React, what we’re really trying to achieve is a smooth user experience where products keep loading automatically as the user scrolls down, instead of clicking a pagination button. Think about Instagram or Amazon product listing pages — when you scroll, more content appears without refreshing the page. That’s the experience we want to build.
Now let’s break this down in a natural flow.
When the component mounts for the first time, React runs the initial logic inside useEffect. This is where we usually trigger our first API call. At this point, the page is empty — no products are loaded yet. So our job is to fetch the first batch of products from Shopify’s Storefront API.
To do that, we send a GraphQL query. In that query, we usually pass parameters like:
-
first→ how many products we want per request (for example, 8 or 10) -
after→ the cursor value (used for pagination) -
handle→ if we are fetching collection products
For the first load, the after cursor will be null because we are loading from the beginning.
So what happens?
The component mounts → fetchProducts() runs → API request goes to Shopify →
Shopify returns product data along with pageInfo.
That pageInfo is extremely important. It gives us two key things:
-
hasNextPage -
endCursor
hasNextPage tells us if more products exist.endCursor gives us the position to fetch the next set.
Once the data returns, we store it inside React state. For example:
-
products→ array of product data -
cursor→ storeendCursor -
hasMore→ storehasNextPage -
loading→ false
At this moment, the first page of products is visible to the user.
Now here’s where infinite scroll logic starts getting interesting.
At the bottom of your product grid, you place a small empty <div> — this is your “loader div.” It doesn’t need to look fancy. It can just contain a spinner or “Loading…” text. But technically, this div acts as a trigger point.
This loader div is important because we will attach something called an Intersection Observer to it.
Now what is Intersection Observer?
It’s a browser API that watches when an element enters the viewport. In simple words, it detects when something becomes visible on screen.
So when the user scrolls down and that loader div comes into view, the observer gets triggered.
Here’s the lifecycle flow:
User opens page → Products render → Loader div is sitting at bottom → User scrolls → Loader enters viewport → Observer fires callback → fetchProducts() runs again.
Now, very important — we don’t want to create an infinite loop. So we must add conditions.
Before calling fetchProducts() again, we check:
-
hasMore === true -
loading === false
If hasMore is false, that means no more products exist. So we stop observing.
If loading is true, that means an API call is already running. We prevent duplicate calls.
So let’s imagine the second fetch.
The observer detects intersection.
It calls fetchProducts().
Now this time, we pass:
-
first = 8 -
after = previousCursor
Shopify returns the next set of products. Now instead of replacing our products state, we append new products to the existing array.
This is key.
Instead of:
setProducts(newProducts)
We do:
setProducts(prev => [...prev, ...newProducts])
That way, the old products stay, and new ones get added at the bottom.
Then we update:
-
cursor = newEndCursor -
hasMore = newHasNextPage -
loading = false
Now here’s the magic part.
Because new products were added, the page height increases.
The loader div automatically moves down.
It is no longer in the viewport.
So nothing happens until the user scrolls again.
1. User scrolls again →
2. Loader enters viewport →
3. Observer triggers →
4. fetch runs again.
This cycle keeps repeating.
Now eventually, Shopify will return:
hasNextPage = false
At that point:
-
hasMorebecomes false. -
We disconnect the observer.
-
Optionally, we hide the loader div.
-
We may show a message like “No more products.”
That’s it. Infinite scroll is complete.
Ultimate Guide to Remix: Remix JS, Remix Project, RemixIDE, Remix Shopify & Remix Run (2026)
Now let’s talk about the React implementation flow in real-world terms.

Step 1: State Setup
You create states like:
-
products -
loading -
cursor -
hasMore
These are the backbone of the system.
Step 2: Initial Fetch
Inside useEffect(() => {}, []), call fetchProducts().
This loads page one.
Step 3: Setup Observer
Create a useRef() for the loader div.
Then inside another useEffect, create an IntersectionObserver.
Attach observer to loaderRef.current.
Cleanup by disconnecting observer on unmount.
Step 4: Fetch Function Logic
Your fetchProducts() function:
-
Check if already loading
-
Set loading true
-
Call Shopify API
-
Append results
-
Update cursor
-
Update hasMore
-
Set loading false
Simple logic, but powerful.
Now let’s talk about real-world performance considerations.
-
Avoid Multiple Observers
Make sure you don’t recreate the observer every render unnecessarily. Dependencies should be carefully managed.
-
Prevent Duplicate Calls
Sometimes observer triggers multiple times quickly. Always guard with loading flag.
-
Handle Network Errors
If API fails, handle it gracefully. Maybe retry or show error message.
-
SEO Consideration
Infinite scroll is not great for SEO because search engines prefer paginated links. So sometimes hybrid approach works better (pagination + infinite scroll).
-
Memory Usage
If there are thousands of products, consider virtualization libraries like react-window.
Now let’s visualize the entire flow clearly:

Clean. Logical. Controlled.
Think of it like a waiter serving dishes at a restaurant buffet.
You don’t bring all dishes at once.
You bring 8 dishes.
Customer eats and asks for more.
You bring next 8.
You repeat until kitchen says “No more food.”
hasMore is the kitchen status.cursor is your place in the order queue.observer is the customer raising hand.fetchProducts is you going back to kitchen.
When the kitchen says no more food, you stop going.
That’s infinite scroll.
Setting Up Shopify Storefront API (GraphQL)
Before writing React logic, we need to prepare our GraphQL query correctly.
In Shopify Storefront API, pagination works using cursor-based pagination, not page numbers. That means every request needs:
-
first→ number of products to fetch -
after→ cursor value -
handle→ collection handle
Here is a proper GraphQL query for fetching collection products:
Now let’s create a reusable function to call Shopify API.
API Utility Function
This function returns:
-
edges→ product data -
pageInfo→ pagination info
Now we integrate this into React.
React Infinite Scroll Component
Let’s build the full component step by step.
Let’s Deeply Understand This Code
Now I’ll explain every part carefully.
1. Why use useCallback for fetchProducts?
If we don’t wrap fetchProducts in useCallback, the function will be recreated on every render. That means our IntersectionObserver will also reinitialize unnecessarily.
Using useCallback stabilizes the function reference.
2. Why check if (loading || !hasMore)?
Because sometimes IntersectionObserver triggers multiple times quickly.
If we don’t protect the function:
-
Multiple API calls will fire
-
Products may duplicate
-
Performance issues will occur
So this condition prevents race conditions.
3. Why append products instead of replacing?
This is crucial:
If we replace products, infinite scroll breaks because old items disappear.
Appending ensures smooth stacking behavior.
4. Why use threshold: 1?
threshold: 1 means the loader must be fully visible before triggering.
You can also experiment with:
This preloads products before the user fully reaches bottom.
Better UX.
Webhooks Explained: Shopify, Zapier, Slack & Discord Automation Made Simple (2026 Guide)
Advanced Improvements (Production Level)

Now let’s improve it further.
1. Reset When Handle Changes
If user switches collection, we must reset state.
Then trigger initial fetch again.
2. Add Debouncing Protection
Sometimes rapid scroll causes double triggers.
You can guard using a ref:
3. Show End Message
When no more products:
Small detail, better UX.
4. Error Handling UI
Instead of only console error:
Display:
Performance Optimization
If collection has 1000+ products, infinite scroll may cause performance drop because DOM grows continuously.
Solutions:
-
Use
react-window -
Use
react-virtualized -
Limit max items
-
Combine with “Load More” fallback
SEO Consideration (Important)
Infinite scroll alone is not SEO-friendly.
Google prefers paginated URLs like:
So best practice:
-
Keep backend pagination
-
Add canonical links
-
Or use hybrid solution
For Shopify headless SEO, sometimes better to combine SSR + client infinite scroll.
Since you are working with Shopify and possibly Next.js, you could:
-
SSR first page
-
CSR infinite scroll next pages
Best of both worlds.
Conclusion
Infinite scroll using Shopify Storefront API and React is not just about loading more products — it’s about building a smooth, modern user experience that feels fast, natural, and interactive.
At its core, the entire system depends on three powerful ideas working together:
-
Cursor-based pagination from Shopify
-
Controlled state management in React
-
Intersection Observer to detect scroll position
When the component mounts, we fetch the first set of products. Once products render, the loader div sits quietly at the bottom. The observer watches that loader. As soon as the user scrolls and the loader intersects the viewport, the next batch of products is fetched and appended. The cursor updates, hasMore updates, and the loader moves further down the page. This cycle continues smoothly until Shopify tells us there are no more products left.
The beauty of this approach is that it feels effortless to the user. There are no clicks. No page refreshes. Just continuous browsing.
But from a developer’s perspective, it requires discipline:
-
Prevent duplicate API calls
-
Manage loading state carefully
-
Append data correctly
-
Stop fetching when
hasNextPagebecomes false -
Disconnect observer properly
When implemented correctly, infinite scroll improves engagement and makes product discovery more enjoyable. However, it’s also important to balance performance and SEO considerations, especially in headless Shopify builds.
If you truly understand the flow then you can apply this pattern anywhere. Not just products, but blogs, reviews, search results, or even order history.
At the end of the day, infinite scroll is less about the code and more about controlling data flow intelligently.
And once you master that flow, you’re not just implementing a feature — you’re designing experience.



