DEV Community

Cover image for How to Dynamically updated Next.js Static Pages with SWR
iskurbanov
iskurbanov

Posted on

How to Dynamically updated Next.js Static Pages with SWR

Article brought to you by buildnextshop.com

This is a continuation to the Shopify + Next.js + Tailwind CSS article that I wrote in September 2021. It already has over 7000 views!

If you have been using React, you probably already know that Next.js is a pretty sweet framework that provides a bunch of features on top of React. Some of the popular ones include: SSR (Server-side Rendering), SSG (Static-site Generation), simplified dynamic routing, easy deployment with Vercel, and much more.

In this article I would like to introduce you to SWR (stale-while-revalidate) package that is also created by the Vercel team.

SWR allows us to add CSR (Client-side rendering) to our static pages generated by Next.js.

So why would we want to add SWR? Well, SSG pages give us a great speed advantage, which is super important in e-commerce. But it also has a disadvantage that we need to rebuild and redeploy any changes that happen to the static pages. This becomes a problem when we want to update small components of our app.

I think this example will give you a good understanding of the power of SWR so let's dive right in!

To follow along with this example, you will need to:

1. Setting up our Next.js /api/available.js file

The /api folder in Next.js is a bit like magic. It allows us to build an API endpoint right in our frontend application. They are server-side only bundles and won't increase your client-side bundle size.

build-next-shop
 ┣ lib
 ┣ node_modules
 ┣ pages
 ┃ ┗ api 
 ┃   ┗ hello.js *
 ┣ public
 ┣ .env.local
 ┗ package.json
....

Enter fullscreen mode Exit fullscreen mode

Let's remove everything in the hello.js file, rename it to available.js and paste in this code:

export default async function available(req, res) {
  const { query: { id } } = req

  const domain = process.env.SHOPIFY_STORE_DOMAIN
  const storefrontAccessToken = process.env.SHOPIFY_STOREFRONT_ACCESSTOKEN

  async function ShopifyData(query) {
    const URL = `https://${domain}/api/2021-07/graphql.json`

    const options = {
      endpoint: URL,
      method: "POST",
      headers: {
        "X-Shopify-Storefront-Access-Token": storefrontAccessToken,
        "Accept": "application/json",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ query })
    }

    try {
      const data = await fetch(URL, options).then(response => {
        return response.json()
      })

      return data
    } catch (error) {
      throw new Error("Products not fetched")
    }
  }

  async function getProduct(handle) {
    const query = `
  {
    productByHandle(handle: "${handle}") {
      id
      variants(first: 25) {
        edges {
          node {
            id
            availableForSale
          }
        }
      }
    }
  }`

    const response = await ShopifyData(query)

    const product = response.data.productByHandle ? response.data.productByHandle : []

    return product
  }

  const products = await getProduct(id)

  res.status(200)
  res.json(products)
}
Enter fullscreen mode Exit fullscreen mode

So what is happening in this code?

  1. We are creating and exporting and async function available with two parameters: request and response.

  2. We are deconstructing the req.query.id variable to get the id

  3. Grabbing our secret values from our .env file and assigning them to domain and storefrontAccessToken variables.

  4. Setting an options variable that contains our Shopify Graphql URL, method of request, headers, and our query body.

  5. We created a getProduct function that receives a handle (which we called id in our case).

  6. We save the results of our getProduct function into a products variable.

  7. We return the products variable in json format to our Next.js component that calls the API.

2. Updating our ProductForm.js component

build-next-shop
 ┣ .next
 ┣ components
 ...
 ┃ ┗ ProductForm.js 
 ┣ context
 ┣ lib
 ┣ node_modules
 ┗ pages
....

Enter fullscreen mode Exit fullscreen mode

Let's import useSWR from "swr" and import axios from "axios" add our custom fetcher function at the top of the component

import useSWR from "swr"
import axios from "axios"

const fetcher = (url, id) => (
  axios.get(url, {
    params: {
      id: id
    }
  }).then((res) => res.data)
)
Enter fullscreen mode Exit fullscreen mode

This function replaces the default swr fetcher function and replaces it with axios (read more about it here)

Then we will use the useSWR hook inside of our component:

...
export default function ProductForm({ product }) {

  const { data: productInventory } = useSWR(
    ['/api/available', product.handle],
    (url, id) => fetcher(url, id),
    { errorRetryCount: 3 }
  )
...
// rest of the component not shown
Enter fullscreen mode Exit fullscreen mode

Now we can console.log(productInventory) and grab the data from our API!

3. Add a useEffect hook to update our state

Let's add a new state:

  const [available, setAvailable] = useState(true)
Enter fullscreen mode Exit fullscreen mode

and then we can update it with our useEffect hook like this:

 useEffect(() => {
    if (productInventory) {
      const checkAvailable = productInventory?.variants.edges.filter(item => item.node.id === selectedVariant.id)

      if (checkAvailable[0].node.availableForSale) {
        setAvailable(true)
      } else {
        setAvailable(false)
      }
    }
  }, [productInventory, selectedVariant])`
Enter fullscreen mode Exit fullscreen mode

First we are checking if productInventory was fetched. Then we find the variant that is selected and search for it in our productInventory variable and update our button state based on the result.

4. Update the button UI based on the availability like this:

return (
    <div className="rounded-2xl p-4 shadow-lg flex flex-col w-full md:w-1/3">
      <h2 className="text-2xl font-bold">{product.title}</h2>
      <span className="pb-3">{formatter.format(product.variants.edges[0].node.priceV2.amount)}</span>
      {
        product.options.map(({ name, values }) => (
          <ProductOptions
            key={`key-${name}`}
            name={name}
            values={values}
            selectedOptions={selectedOptions}
            setOptions={setOptions}
          />
        ))
      }
      {
        available ?
          <button
            onClick={() => {
              addToCart(selectedVariant)
            }}
            className="bg-black rounded-lg text-white px-2 py-3 mt-3 hover:bg-gray-800">
            Add To Card
          </button>
          :
          <button
            className="rounded-lg text-white px-2 py-3 mt-3 bg-gray-800 cursor-not-allowed">
            Sold out!
          </button>
      }
    </div>
  )
Enter fullscreen mode Exit fullscreen mode

Here we create a ternary to check our available state and choose which button to display based on the boolean value.

I hope you enjoyed the tutorial!

Sample Starter Project: https://github.com/iskurbanov/shopify-next.js-tailwind

Checkout the example website and full tutorial at BuildNextShop.com where we create a fully production ready Shopify Headless store using Next.js!

Oldest comments (0)