Gatsby and Shopify Part 3
Add to favorites
Create a seamless checkout experience with Shopify and Gatsby

Advanced React Hooks Handbook
1
Intro to Firebase
6:59
2
Firebase Auth
11:59
3
Firestore
10:51
4
Firebase Storage
6:40
5
Serverless Email Sending
10:02
6
Geocoding with Mapbox
9:24
7
Contentful Part 1
4:59
8
Contentful Part 2
8:52
9
Gatsby and Shopify Part 1
5:20
10
Gatsby and Shopify Part 2
13:21
11
Gatsby and Shopify Part 3
14:32
12
Creating Stripe Account and Product
5:18
13
Adding Stripe.js to React
5:54
14
Stripe Checkout Client Only
18:18
15
PayPal Checkout
31:21
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:
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 .
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.
Templates and source code
Download source files
Download the videos and assets to refer and learn offline without interuption.
Design template
Source code for all sections
Video files, ePub and subtitles
Browse all downloads
1
Intro to Firebase
Learn about Firebase and set it up in your React project
6:59
2
Firebase Auth
Set up authentication in your React application using Firebase Auth
11:59
3
Firestore
Enable Firestore as your database in your React application
10:51
4
Firebase Storage
Enable Firebase Storage in your application to store photos or videos from users
6:40
5
Serverless Email Sending
Use EmailJS to send an email without backend
10:02
6
Geocoding with Mapbox
Create an address autocomplete with Mapbox's Geocoding API
9:24
7
Contentful Part 1
Create a Contentful account, add a model and content
4:59
8
Contentful Part 2
How to use the Content Delivery API to fetch data from Contentful
8:52
9
Gatsby and Shopify Part 1
Create a Shopify store, generate a password and add products to the store
5:20
10
Gatsby and Shopify Part 2
Connect Shopify to Gatsby and display all products
13:21
11
Gatsby and Shopify Part 3
Create a seamless checkout experience with Shopify and Gatsby
14:32
12
Creating Stripe Account and Product
Start integrating with Stripe and set up an account, product and price
5:18
13
Adding Stripe.js to React
Loading Stripe.js library to your React client application
5:54
14
Stripe Checkout Client Only
Accept payment with Stripe Checkout without backend
18:18
15
PayPal Checkout
Integrate online payment with PayPal
31:21
Meet the instructor
We all try to be consistent with our way of teaching step-by-step, providing source files and prioritizing design in our courses.
Stephanie Diep
iOS and Web developer
Developing web and mobile applications while learning new techniques everyday
7 courses - 36 hours

Build Quick Apps with SwiftUI
Apply your Swift and SwiftUI knowledge by building real, quick and various applications from scratch
11 hrs

Advanced React Hooks Handbook
An extensive series of tutorials covering advanced topics related to React hooks, with a main focus on backend and logic to take your React skills to the next level
3 hrs

SwiftUI Concurrency
Concurrency, swipe actions, search feature, AttributedStrings and accessibility were concepts discussed at WWDC21. This course explores all these topics, in addition to data hosting in Contentful and data fetching using Apollo GraphQL
3 hrs

SwiftUI Combine and Data
Learn about Combine, the MVVM architecture, data, notifications and performance hands-on by creating a beautiful SwiftUI application
3 hrs

SwiftUI Advanced Handbook
An extensive series of tutorials covering advanced topics related to SwiftUI, with a main focus on backend and logic to take your SwiftUI skills to the next level
4 hrs

React Hooks Handbook
An exhaustive catalog of React tutorials covering hooks, styling and some more advanced topics
5 hrs

SwiftUI Handbook
A comprehensive series of tutorials covering Xcode, SwiftUI and all the layout and development techniques
7 hrs