Using SWR Library in Sitecore Headless Next JS Apps for data fetching
For any web application, the data fetching logics are essential and it always has some constant improvements from normal Ajax requests to some modern techniques like axios, fetch, request in the way how the remote data are fetched for a component to work.
In this post, we will be seeing how to use the awesome SWR module with our headless sitecore NextJS applications to build a product grid with pagination options. SWR stands for ‘Stale-while-revalidate’ and it is essentially set of React Hooks that offers different functionalities like caching, revalidation etc.,
In general, the SWR works like,
- It returns the stale data ( from cache)
- it then fetches the data from remote in the background
- finally it returns the most recent data. In this way it allows us to present something to the users while it fetches fresh data from the remote server
You can follow this Site to setup the SWR into the nextjs application and to know some more advanced options like error handling, revalidation etc., For this blog, I have used the repo helix example which has this SWR integration already done. So we really not have to do the ground work to set this up.
Data Fetching Options in SWR
SWR offers the following two react hooks to fetch the remote data,
- useSWR
- useSWRInfinite
There are differences between both of the above hooks. If we would like to build something like below, then we can achieve this with just the useSWR hook itself.

In case if we would like to build something with infinite loading options or doing paginations while scrolling like below, then we have to use the later hook useSWRInfinite.

Both of the above option requires a fetcher function and inside the fetcher function we can use any library to handle the data fetching like fetch, axios and request etc., The very fundamental API of SWR looks like below, here Key is the url and fetcher is an asynchrones function that takes the key, makes the API requests and returns the data.

We will be seeing in detail how to implement the product grid using both of the above options. In the below examples, inside the fetcher function, we will be using ‘request’ from ‘graphql-request’ library as we will be dealing with graphql queries.
Before we deep dive into the different SWR options , I would like to explain the graphql query I have used for this component and an issue I faced with during the product grid component building using useSWR hook with Experience Edge search query and how did I overcome that.
Product List Graphql query and the Experience Edge Search query
The below graphql search query is used to fetch the products with pagination. You can notice that it is trying to fetch first 4 records after the given after cursor value.
| query ProductList($rootPath: String!, $templateId: String!, $after: String) { | |
| search( | |
| where: { | |
| AND: [ | |
| { name: "_templates", value: $templateId, operator: CONTAINS } | |
| { name: "_path", value: $rootPath, operator: CONTAINS } | |
| ] | |
| } | |
| first: 4 | |
| after: $after | |
| orderBy: { name: "Title", direction: ASC } | |
| ) { | |
| results { | |
| url { | |
| path | |
| } | |
| ... on _Product { | |
| title { | |
| value | |
| } | |
| shortDescription { | |
| value | |
| } | |
| image { | |
| src | |
| } | |
| } | |
| } | |
| pageInfo { | |
| endCursor | |
| hasNext | |
| } | |
| total | |
| } | |
| } |
When I was testing the above query with GraphQL playground, I noticed that it is not working as expected. I tried to fetch the first 4 records from the index 4. But it always gives me the first four records, ignoring the ‘after’ keyword which is wrong.
This is the input I have given,

Despite I tried different options to resolve this, It left me like a lost puppy and took more of my time to find the cause of this and the solution. Finally my friend Ram’s blog on the Experience Edge setup has thrown me some lights on this and I was able to find the cause and resolution.

As mentioned in his blog, I took a look at the above namespace and noticed that though the search query is expecting the ‘after’ value to be a string, they are trying to parse it as a base64string format and converting it back to int. This information is not there in any of the sitecore document regarding Experience Edge search query and even in the sample queries.

So I need to input the ‘after’ value in base64 string format. So the above same input changed like this and it started working as expected.

So in my below example, I need to convert the ‘after’ vaule from string to base64 string format like below and pass it to the query,

So please don’t be surprised if you notice the above conversion in my code. Let us now pitch in exploring the different data fetching options the SWR offers.
Option-1 : using the hook useSWR
This useSWR hook takes two parameters a key and a fetcher function and results two results ( data and error).
- A key – This is a unique string value for the data that gets fetched for this particular key. Key can be an url, or a query ( in our case as its graphql). In our case, the key looks like below

- A Fetcher function – This function is where we usually make the requests using the key passed and returns the results as data
Let us define our fetcher function first and it looks like below. It takes the graphql query, root item id and pageIndex as the key,

And the useSWR part looks like,

Note that we are passing array as the key parameter that contains multiple arguments of the fetcher function. Also we are forming the key with pageIndex as well to make the key to be unique. And we will be benefiting the SWR cache with key for a page with the particular index.
We can extract the result data and use it as we need. The entire ProductList component is given below.
The output of the new ProductList component with useSWR hook will be look like,
If you take a closer look at the above video, everytime I nagivate to a new page for first time, there is a flicker. It is because, the SWR is making a fresh API request for first time when a page is visited nevertheless next time when I nagivate to the same page, there are no flickers. It is because the data is being served from SWR cache which is by default.
We can also get rid of the first time flicker by pre loading the next page in advance and hide it and make it visible next time.
Option-2: to use useSWRInfinite hook
As mentioned initially, the useSWRInfinite hook can be used in scenario where we would like to build something with infinite loading with ‘Load more’ button. We cannot achieve this in regular way as each page needs data from previous page data also. In this scenario, the useSWRInfinte hook can help. The sitecore helix example product list component already is using this hook along with the component level data fetching ( which I have explained in my previous blog with the header component as an example). Here I have avoided this component level data fetching as we are only focusing on SWR.
Similer to the previous useSWR, this hook accepts a function that returns a key and a fetcher function and some options. It also includes two extra values, the page size and a setter function. The useSWRInfinite hooks parameters are,
- getKey – This is a function that expects the index and previous page data and it returns back a key ( like useSWR’s key)
- fetcher – A function similer to useSWR’s
- Options – Additional configuration options. You can refer this page to know more about this.
Let us see how the getKey function is defined and you can notice below that it takes pageIndex and previousPageDate as argument and returns the key based on this.

Next is to define the fetcher function and it looks like below,

With this now we can go ahead and define our useSWRInfinite and it is given below,

Similar to useSWR, we can retrieve the data and use it as needed.
The full ProductListInfiniteLoader component is given below,
| import React from 'react'; | |
| import { | |
| ComponentRendering, | |
| useSitecoreContext, | |
| } from '@sitecore-jss/sitecore-jss-nextjs'; | |
| import { request } from 'graphql-request'; | |
| import { useSWRInfinite } from 'swr'; | |
| import { ProductListDocument, ProductListQuery, _Product, Item } from './ProductList.graphql'; | |
| import { SitecoreContextValues } from 'lib/page-props'; | |
| import { SitecoreTemplates } from 'lib/sitecoreTemplates'; | |
| import config from 'temp/config'; | |
| import { DocumentNode } from 'graphql'; | |
| import ListProduct from 'lib/helpers/ListProduct'; | |
| export type ProductListProps = { | |
| rendering: ComponentRendering; | |
| }; | |
| type keyType = string | any[] | null; | |
| // SWR will create arguments for each page based on this | |
| const getKey = ( | |
| itemId: string | undefined, | |
| pageIndex: number, | |
| previousPageData: ProductListQuery | |
| ): keyType => { | |
| // reached the end | |
| if (previousPageData && !previousPageData.search?.pageInfo.hasNext) return null; | |
| console.log('endCursor ' + JSON.stringify(previousPageData?.search?.pageInfo.endCursor)); | |
| console.log('previousPageData ' + JSON.stringify(previousPageData)); | |
| // first page, we don't have `previousPageData` | |
| if (pageIndex === 0) return [ProductListDocument, itemId, null]; | |
| return [ProductListDocument, itemId, previousPageData?.search?.pageInfo.endCursor]; | |
| }; | |
| // SWR will execute this using the arguments formed above | |
| const productListQuery = (query: DocumentNode, itemId: string, after?: string) => { | |
| // normalize item id | |
| itemId = itemId.split('-').join('').toLowerCase(); | |
| // proxy the request through Next.js if running in browser | |
| const endpoint = | |
| typeof window === 'undefined' | |
| ? config.graphQLEndpoint | |
| : `${config.graphQLEndpointPath}?sc_apikey=${config.sitecoreApiKey}`; | |
| const data = request(endpoint, query, { | |
| templateId: SitecoreTemplates._Product.Id, | |
| rootPath: itemId, | |
| after, | |
| }); | |
| console.log('After ' + JSON.stringify(after)); | |
| return data; | |
| }; | |
| const ProductListInfiniteLoader = ({ rendering }: ProductListProps): JSX.Element => { | |
| const itemId = useSitecoreContext |
|
| const getKeyForItem = (pageIndex: number, previousPageData: any): keyType => { | |
| console.log('After ' + JSON.stringify(previousPageData)); | |
| return getKey(itemId, pageIndex, previousPageData); | |
| }; | |
| const { data, size, setSize } = useSWRInfinite |
|
| getKeyForItem, | |
| productListQuery, | |
| { | |
| revalidateAll: true, | |
| } | |
| ); | |
| // console.log('Data from Component ' + JSON.stringify(data)); | |
| return ( | |
| {data && | |
| data.map((page) => { | |
| return page.search?.results?.map((productItem) => { | |
| const product = productItem as _Product; | |
| const item = productItem as Item; | |
| return ( | |
| |
|
{product.title?.value} |
|
| {product.shortDescription?.value} |
|
| ); | |
| }); | |
| })} | |
| {data && data[data.length - 1].search?.pageInfo.hasNext && ( | |
| Load More | |
| )} | |
| ); | |
| }; | |
| export default ProductListInfiniteLoader; |
The output of this component will look like this,
I hope now its clear how the SWR Library made easy to build a component with pagination option etc., It also offers other advantages like prefetching, revalidation etc., which you can refer more from the swr official site.
Thanks for reading and stay tuned for more stuffs….!!!!
References
