The Storefront API and React: Setting up your products
In this tutorial we are going to set up products with their available options. This will allow shoppers to select the product configuration they want. One step closer to adding products to the cart 🙌
What id do we send to the cart to add a product?
Before we start writing code, let's talk a bit about what information we need about a product to let a shopper add it to their cart. In the Shopify ecosystem product configurations can be defined by product options and variants. Let's use an example to illustrate this:
We have a T-shirt.
Our T-shirt comes in 3 different sizes (sm, md, lg) and 2 different colors (black, white).
In this example color and size are the product options. Products can have up to 3 options.
Each combination of options selections is a variant. So a sm black t-shirt would be one variant of the t-shirt product.
In Shopify, when a customer wants to purchase a product we need to send back the variant id for the configuration of the product they want. In the case of our small, black t-shirt we would send back and id that was specific to only the t-shirt product with the color option black and size option small.
Set up the product component
Let's create the base of the product component we will render for each product. To keep our code modular we'll create a new file for our product component. In the src folder create a file called Product.js. Include the following code to set up the product component.
import React, { Component } from 'react';
class Product extends Component {
render() {
return (
[the product title]
);
}
}
export default Product;
Now we'll bring the Product component into our App component so we can pass through the product data and actually render product images and titles.
In your App.js component, modify the code to look like the following:
...
import Product from './Product.js';
class App extends Component {
render() {
// waiting for the shop data
if (this.props.shopData && this.props.shopData.loading) {
return Loading ✨
}
// error retrieving shop data
if (this.props.shopData && this.props.shopData.error) {
return Sorry something went wrong. Check back soon.
}
// display products
const productsToDisplay = this.props.shopData.shop.products
return (
{productsToDisplay.edges.map((el, i)=> {
return(
)
})}
);
}
}
Instead of rendering the HTML to make our product directly in the App component we'll pass the product data through to the Product component and build the HTML there. Note, we passed all of the product data through to the Product component in a prop named product.
Pulling it all together, back in the Product.js file let's use the data stored in props to set the product image and title. Modify the code to look like this:
import React, { Component } from 'react';
class Product extends Component {
render() {
const {images, title} = this.props.product
return (
{title}
);
}
}
export default Product;
Modify the GraphQl query to pull in options data
Next we need to grab all of the options available on each product. To do that modify the query to include the product options:
const HOME_QUERY = gql`
query ShopData {
shop {
name
description
products(first:20) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
id
title
options{
id
name
values
}
images(first: 1) {
edges {
node {
src
}
}
}
}
}
}
}
}
`;
Add selections for options to each product
Now that we have the option data we need to actually display it for the customers to make their selections. Back in Product.js let's include some code to render select elements for each set of options. In the component render function define const for options and then create a constant for the HTML for each option select element. Loop through the options to create each select element. Then, within each select, loop through the option values to ceate all the option elements. The code will look like this:
const {images, title, options} = this.props.product
const productOptions = options.map((option, i) => {
return(
)
})
Finally include productOptions under the product title.
Include the product variants
Those selects look good, but we still don't have it set up to actually retrieve the variant. To do this we will first include the product variants in our GraphQl query. Back in App.js adjust your query to the following:
const HOME_QUERY = gql`
query ShopData {
shop {
name
description
products(first:20) {
pageInfo {
hasNextPage
hasPreviousPage
}
edges {
node {
id
title
options{
id
name
values
}
variants(first:250){
pageInfo{
hasNextPage
hasPreviousPage
}
edges{
node{
id
title
price
selectedOptions{
name
value
}
}
}
}
images(first: 1) {
edges {
node {
src
}
}
}
}
}
}
}
}
`;
This will include the first 250 variants for each product in the query along with their id, title, price, and selected options. Selected options will return the name and value for each option represented in the variant.
Set the select options and the corresponding variant on the Product state
in Product.js we are going to store the options the customer has selected and the corresponding variant node on state. To start let's set up the state on our component:
class Product extends Component {
constructor() {
super();
this.state = {};
}
...
}
We'll include the constructor function for the product class and set the state to an empty object.
Next let's set the first values for each available option to the state when the component mounts
class Product extends Component {
constructor() {
super();
this.state = {};
}
...
componentDidMount() {
const selectedOptionsObj = this.props.product.options.reduce((prev, curr, i) => {
prev[curr.name] = curr.values[0]
return prev
}, {})
this.setState({
selectedOptions: selectedOptionsObj
})
}
...
}
To create the selectedOptionsObj object we will grab the name of each option as the key and set the value as the first available value. Now that we are storing the selected options in state, we need a function to update the selectedOptions object when the customer picks a different option. After the constructor function we'll make a new function to handle changing the options
handleVariantChange(e) {
const target = e.target
const selectedOptions = this.state.selectedOptions;
selectedOptions[target.name] = target.value
const selectedVariant = this.props.product.variants.edges.find((variant) => {
return variant.node.selectedOptions.every((selectedOption) => {
return selectedOptions[selectedOption.name] === selectedOption.value;
});
})
this.setState({
selectedVariant: selectedVariant
})
}
Once the handleVariantChange function is set up, let's bind this to the function in our constructor
constructor() {
super();
this.state = {};
this.handleVariantChange = this.handleVariantChange.bind(this);
}
To pull it all together set up the function to fire on change of the option select:
...
...
Test it out! When you change the option on a product you should the the selectedOptions object on the product state update.
Let's make one final adjustment to our product state on component mount. We want to store the selected variant when the component mounts so that we can still use it event if the customer doesn't change the options from the default. To do this, let's break out the code that finds the correct variant and sets it to state in our handleVariantChange function and make a new setSelectedVariant function. The two functions will look like this:
setSelectedVariant(selectedOptions) {
const selectedVariant = this.props.product.variants.edges.find((variant) => {
return variant.node.selectedOptions.every((selectedOption) => {
return selectedOptions[selectedOption.name] === selectedOption.value;
});
})
this.setState({
selectedVariant: selectedVariant
})
}
handleVariantChange(e) {
const target = e.target
const selectedOptions = this.state.selectedOptions;
selectedOptions[target.name] = target.value
this.setSelectedVariant(selectedOptions)
}
On component mount, run the setSelectedVariant function to set this to the default options variant.
componentDidMount() {
const selectedOptionsObj = this.props.product.options.reduce((prev, curr, i) => {
prev[curr.name] = curr.values[0]
return prev
}, {})
this.setState({
selectedOptions: selectedOptionsObj
})
this.setSelectedVariant(selectedOptionsObj);
}
Great! Now you'll always have access to the variant for the selected options on the product. This will come in super handy when we set up the ability to add a product to the cart.
Mitigating for options with only one value
It might seems kind of silly if you are displaying option dropdowns for options that only have one possible value. Let's hide the select elements for options with only one value. Conditionally add a class of hidden to your select if the option values array has a length less than or equal to 1.
Then in App.css create a declaration for hidden to hide the element.
.hidden {
display: none;
}
💥 Tada: you can now select product configurations and easily access the appropriate variant on each product state! Next we'll go over how to use the variant information to add the product to the cart.