Design+Code logo

Quick links

Suggested search

Shopify setup and connecting to frontend

If you haven't already, I highly encourage you to follow the first part of this tutorial , where you'll learn how to setup a Shopify store, add products to your store, and get a password to connect your Gatsby website to Shopify. Then, follow up with the second part of this tutorial , where we connect our Shopify store to our frontend by creating a GraphQL query, a products list page, as well as a unique page for each product in our store.

Completed project code

You can find the completed project code on Github at https://github.com/stephdiep/gatsby-shopify-tutorial . You can also download the source files, but remember to add your own password and storeUrl in the .env file for the project to build successfully. You can view the live demo at https://gatsby-shopify-tutorial.netlify.app/.

Install dependencies

We'll need to install two new packages, isomorphic-fetch and shopify-buy . Run the following command in the Terminal.

npm install isomorphic-fetch shopify-buy

Get the Storefront access token

On Shopify's dashboard, click on Apps > Manage private apps , at the bottom. Click on your private app name, then scroll down to the Storefront API section (at the bottom of the page). You'll see your Storefront access token at the bottom. Copy it, and add it to the .env file of your project.

GATSBY_STOREFRONT_ACCESS_TOKEN=4759383745k9393838add939

Create a context

To create a checkout process, we first need to create a context. You can learn more about how to create a context by heading over to the useReducer with useContext tutorial ( part 1 , part 2 , part 3 ) of this handbook. Create a new folder called context , and under that folder, a file called StoreContext.js . Add the following imports at the top.

// src/context/StoreContext.js

import React, { createContext, useState, useEffect, useContext } from "react"
import fetch from "isomorphic-fetch"
import Client from "shopify-buy"

We'll need to create a Shopify client. Both the domain and the storefrontAccessToken are stored in our .env file, so we'll get them from there.

// src/context/StoreContext.js

const client = Client.buildClient(
  {
    domain: process.env.GATSBY_SHOPIFY_STORE_URL,
    storefrontAccessToken: process.env.GATSBY_STOREFRONT_ACCESS_TOKEN,
  },
  fetch
)

Create an object of default values that we'll need for our checkout state later on.

// src/context/StoreContext.js

const defaultValues = {
  cart: [],
  loading: false,
  addVariantToCart: () => { },
  removeLineItem: () => { },
  client,
  checkout: {
    id: "",
    lineItems: [],
    webUrl: ""
  },
}

Let's create a StoreContext.

// src/context/StoreContext.js

const StoreContext = createContext(defaultValues)

We'll need two variables, one that lets us now if the build is for a browser - because we just want to enable checkout if we are in a browser, and another one for our localStorageKey.

// src/context/StoreContext.js

const isBrowser = typeof window !== `undefined`
const localStorageKey = `shopify_checkout_id`

Then, it's time to create our provider.

// src/context/StoreContext.js

export const StoreProvider = ({ children }) => {

}

Inside of the provider, we need to add three states.

// src/context/StoreContext.js inside StoreProvider

const [cart, setCart] = useState(defaultValues.cart)
const [checkout, setCheckout] = useState(defaultValues.checkout)
const [loading, setLoading] = useState(false)

Still inside of the provider, add the following function. This checks if we're in a browser - if we are, we're setting the checkout id in our browser's local storage, and also setting the checkout state.

// src/context/StoreContext.js inside StoreProvider

const setCheckoutItem = (checkout) => {
	if (isBrowser) {
		localStorage.setItem(localStorageKey, checkout.id)
	}

	setCheckout(checkout)
}

Right below setCheckoutItem , inside of the provider, we'll create a useEffect . This useEffect checks if we already created a checkout. If we do, we are refetching that checkout object. Otherwise, we're creating a new one.

// src/context/StoreContext.js inside StoreProvider

useEffect(() => {
  const initializeCheckout = async () => {
    const existingCheckoutID = isBrowser
      ? localStorage.getItem(localStorageKey)
      : null

    if (existingCheckoutID && existingCheckoutID !== `null`) {
      try {
        const existingCheckout = await client.checkout.fetch(
          existingCheckoutID
        )
        if (!existingCheckout.completedAt) {
          setCheckoutItem(existingCheckout)
          return
        }
      } catch (e) {
        localStorage.setItem(localStorageKey, null)
      }
    }

    const newCheckout = await client.checkout.create()
    setCheckoutItem(newCheckout)
  }

  initializeCheckout()
}, [])

Then, we create the addVariantToCart function to add an item to our checkout cart. This functions accepts a product and a quantity as arguments. It's important to note that the addLineItems function from Shopify takes the shopifyId from the variants array, that we fetched with our GraphQL query. We'll update our Shopify cart, as well as our cart state.

// src/context/StoreContext.js inside StoreProvider

const addVariantToCart = async (product, quantity) => {
  setLoading(true)

  if (checkout.id === "") {
    console.error("No checkout ID assigned.")
    return
  }

  const checkoutID = checkout.id
  const variantId = product.variants[0]?.shopifyId
  const parsedQuantity = parseInt(quantity, 10)

  const lineItemsToUpdate = [
    {
      variantId,
      quantity: parsedQuantity,
    },
  ]

  try {
    const res = await client.checkout.addLineItems(checkoutID, lineItemsToUpdate)
    setCheckout(res)

    let updatedCart = []
    if (cart.length > 0) {
      const itemIsInCart = cart.find((item) => item.product.variants[0]?.shopifyId === variantId)

      if (itemIsInCart) {
        const newProduct = {
          product: { ...itemIsInCart.product },
          quantity: (itemIsInCart.quantity + parsedQuantity)
        }
        const otherItems = cart.filter((item) => item.product.variants[0]?.shopifyId !== variantId)
        updatedCart = [...otherItems, newProduct]
      } else {
        updatedCart = cart.concat([{ product, quantity: parsedQuantity }])
      }
    } else {
      updatedCart = [{ product, quantity: parsedQuantity }]
    }
    setCart(updatedCart)

    setLoading(false)
    alert("Item added to cart!")
  } catch (error) {
    setLoading(false)
    console.error(`Error in addVariantToCart: ${error}`)
  }
}

Right after that function, we'll add one last one. This function accepts a variantId as an argument. Then, in the lineItems array from the checkout object, we'll find its corresponding lineItemID , needed for the removeLineItems function from Shopify. We'll then update both our Shopify cart and cart state.

const removeLineItem = async (variantId) => {
  setLoading(true)
  try {
    let lineItemID = ''
    checkout.lineItems?.forEach((item) => {
      if (item.variableValues.lineItems[0]?.variantId === variantId) {
        lineItemID = item.id
      }
    })

    if (!lineItemID) {
      console.log('Product not in cart')
      return
    }

    const res = await client.checkout.removeLineItems(checkout.id, [lineItemID])
    setCheckout(res)

    const updatedCart = cart.filter((item) => item.product.variants[0]?.shopifyId !== variantId)
    setCart(updatedCart)
    setLoading(false)
  } catch (error) {
    setLoading(false)
    console.error(`Error in removeLineItem: ${error}`)
  }
}

At the bottom of the provider, don't forget to return the provider with a value object.

// src/context/CombinedProvider.js

return (
  <StoreContext.Provider
    value={{
      ...defaultValues,
      addVariantToCart,
      removeLineItem,
      cart,
      checkout,
      loading,
    }}
  >
    {children}
  </StoreContext.Provider>
)

Then, connect the StoreContext with the useContext hook. We'll call this custom hook useStore.

// src/context/StoreContext.js

const useStore = () => {
  const context = useContext(StoreContext)

  if (context === undefined) {
    throw new Error("useStore must be used within StoreContext")
  }

  return context
}

export default useStore

We'll need to wrap our entire application with the StoreProvider . Under context, create a new file called CombinedProvider.js . Add the following code.

// src/context/CombinedProvider.js

import React from "react"

import { StoreProvider } from "./context/StoreContext"

const CombinedProvider = ({ element }) => {
  return (
    <StoreProvider>{element}</StoreProvider>
  )
}

export default CombinedProvider

Then, in gatsby-ssr.js and gatsby-browser.js , we'll wrap our entire application with the CombinedProvider , meaning that everything in our provider will be available everywhere in our application.

// gatsby-ssr.js

import CombinedProvider from "./src/context/CombinedProvider"

export const wrapRootElement = CombinedProvider
// gatsby-browser.js

import CombinedProvider from "./src/context/CombinedProvider"

export const wrapRootElement = CombinedProvider

Code the cart page UI

Let's code the UI for the cart page. You can copy the components below, or create your own. First, create the ProductRow.

// src/components/ProductRow.js

import React from "react";
import styled from "styled-components"

const ProductRow = ({ item }) => {
	const { product, quantity } = item

  return <Wrapper>
    <ProductWrapper>
      <Image src={product.images[0]?.src} alt={product.title} />
      <Subtitle>{product.title}</Subtitle>
    </ProductWrapper>
    <Subtitle>{quantity}</Subtitle>
    <DeleteButton onClick={() => console.log("Remove item")}>Remove</DeleteButton>
  </Wrapper>
}

export default ProductRow

const Wrapper = styled.div`
  display: grid;
  grid-template-columns: repeat(3, 330px);
  gap: 40px;
  align-items: center;
`

const ProductWrapper = styled.div`
  display: grid;
  grid-template-columns: 80px auto;
  gap: 20px;
  align-items: center;
  width: 330px;
`

const Image = styled.img`
  width: 80px;
  height: 80px;
  object-fit: cover;
  border-radius: 20px;
`

const Subtitle = styled.p`
  font-weight: bold;
  font-size: 14px;

`

const DeleteButton = styled.p`
  color: #a61b2b;
  font-size: 14px;
  cursor: pointer;
`

Next, code the cart page.

// src/pages/cart.js

import React from 'react'
import styled from "styled-components"

import Layout from '../components/layout'
import ProductRow from '../components/ProductRow'
import PrimaryButton from "../components/PrimaryButton"

const Cart = () => {
  return (
    <Layout>
      <Wrapper>
        <HeaderWrapper>
          <Text>Product</Text>
          <Text>Quantity</Text>
          <Text>Remove Item</Text>
        </HeaderWrapper>

        { /* Add the contents of the cart here... */ }

        <ButtonWrapper>
          <PrimaryButton text="Checkout" onClick={() => console.log("Redirect to checkout page")} />
        </ButtonWrapper>
      </Wrapper>
    </Layout>
  )
}

export default Cart

const Wrapper = styled.div`
  margin: 40px;
`

const HeaderWrapper = styled.div`
  display: grid;
  grid-template-columns: repeat(3, 330px);
  gap: 40px;
`

const Text = styled.p`
  font-weight: 600;
  font-size: 14px;
`

const ButtonWrapper = styled.div`
  display: flex;
  justify-content: flex-end;
`

Don't forget to add a link to the /cart page in your website's header, if you haven't done it already. In the cart page, we'll import the useShop hook and iterate over the cart items. If the cart is empty, we'll let the user know.

// src/pages/cart.js

import useStore from '../context/StoreContext'

const Cart = () => {
  const { cart } = useStore()

  return (
    <Layout>
      <Wrapper>
        { /* More content... */ }
        {
          cart.length > 0 ? cart.map((item, index) => <ProductRow key={index} item={item} />) : <Text>Your cart is empty.</Text>
        }
        { /* More content... */ }
      </Wrapper>
    </Layout>
  )
}

Add to cart functionality

Let's implement the add to cart functionality! In our product card, import the useStore hook, get the addVariantToCart function, and pass it to the onClick event of the AddButton . Remember to pass the product object to the function, as well as the quantity. Since the current component is the ProductCard , we'll default the quantity to 1.

// src/components/ProductCard.js

import useStore from '../context/StoreContext'

const ProductCard = ({ product }) => {
  const { addVariantToCart } = useStore()

  return (
    <Wrapper>
      <AddButton onClick={() => addVariantToCart(product, 1)}><p>+</p></AddButton>
      { /* Content here... */ }
    </Wrapper>
  )
}

Let's do the same for the the product page. But first, we'll need to add the useInput hook (that can be found in this section of the handbook ) to our project and use it. Let's default the value of the form to 1 , and pass the value and onChange event to the input.

// src/templates/product.js

import useInput from "../utils/useInput"

const ProductTemplate = ({ pageContext }) => {
  const { product } = pageContext
  const bind = useInput(1)

  return (
    <Layout>
			{ /* More content... */ }
          <InputForm>
            <Subtitle><label htmlFor="qty">Quantity:</label></Subtitle>
            <Input placeholder="1" id="qty" type="number" {...bind} />
          </InputForm>
			{ /* More content... */ }
    </Layout>
  )
}

Then, import the useStore hook, get the addVariantToCart function, and add it to the onClick event of the PrimaryButton . Pass the product to the function, and bind.value to get the quantity the user wants.

// src/templates/product.js

import useStore from "../context/StoreContext"
import useInput from "../utils/useInput"

const ProductTemplate = ({ pageContext }) => {
  const { product } = pageContext
  const { addVariantToCart } = useStore()
  const bind = useInput(1)

  return (
    <Layout>
     	{ /* More content... */ }
          <PrimaryButton text="Add to cart" onClick={() => addVariantToCart(product, bind.value)} />
			{ /* More content... */ }
    </Layout>
  )
}

Remove item from cart functionality

In ProductRow , we'll import removeLineItem from the useStore hook. Then, we'll call the function in on the onClick event of the DeleteButton . Remember to pass the shopifyId to the function.

// src/components/ProductRow.js

import useStore from "../context/StoreContext";

const ProductRow = ({ item }) => {
  const { removeLineItem } = useStore()
  const { quantity, product } = item

  return <Wrapper>
    { /* More content... */ }
    <DeleteButton onClick={() => removeLineItem(product.variants[0]?.shopifyId)}>Remove</DeleteButton>
  </Wrapper>
}

Navigate to Shopify checkout page

When the user clicks on the Checkout button, we want to redirect them to the checkout page from Shopify, where they'll be able to enter their contact and payment information. This is what a Shopify checkout page look like:

Shopify Checkout page In cart.js , let's get the checkout object from the useStore hook.

// src/pages/cart.js

const { cart, checkout } = useStore()

We want to disable the button if the cart is empty. Add the following attribute to the PrimaryButton in cart.js.

// src/pages/cart.js, add as attribute to PrimaryButon

disabled={cart.length === 0}

On click of the primary button, we'll redirect the user to the checkout page. We access the link through the checkout object, by doing checkout.webUrl.

// src/pages/cart.js

<PrimaryButton text="Checkout" onClick={() => window.open(checkout.webUrl)} disabled={cart.length === 0} />

If you click on the Checkout button, it'll lead to a Shopify page. However, you'll see an error page stating that This store isn't taking any orders right now .

Shopify Checkout page error As stated in this thread in Shopify Community , this error is shown because we are under the 14-days free trial period. To enable checkout, we'll need to upgrade to a paid plan - either the Basic plan or higher. Then, the user will see the checkout page, where they'll be able to fill in their contact and payment information.

Conclusion

Congratulations! You just completed the Gatsby Shopify three-part tutorial! In this series, we learned how to create our Shopify store, add products to it, connect the data to our Gatsby website, and create a checkout experience. You can now go ahead and build amazing e-commerce websites!

Learn with videos and source files. Available to Pro subscribers only.

Purchase includes access to 50+ courses, 320+ premium tutorials, 300+ hours of videos, source files and certificates.

BACK TO

Gatsby and Shopify Part 2

READ NEXT

Creating Stripe Account and Product

Templates and source code

Download source files

Download the videos and assets to refer and learn offline without interuption.

check

Design template

check

Source code for all sections

check

Video files, ePub and subtitles

Browse all downloads