I recently made a simple eCommerce app using React, WPGraphQL, WooGraphQL, and the Ionic Framework.
It was a fun learning experience, this is a nice stack to work with. I have worked with WPGraphQL before, but I still have a lot to learn, so in this post I will share what I learned in case it might help you. Props to Jacob Arriola for his example code and help!
Grab the code from my github repository.
I built this app with the Ionic Framework, so it could become an iOS/Android mobile app, but it’s just a React app. If you have a different framework you like this could easily be converted to Gatsby, Next, etc.
WPGraphQL
WPGraphQL is an alternative to the WordPress REST API. It only has one endpoint, yoursite.com/graphql, and it only accepts POST requests. It returns only the data you request, so it can be a lot faster than a REST API, and easier to work with since you don’t have to create custom endpoints.
To use WPGraphQL, download and install the free plugin on your site. To use WooCommerce, you will also need the WooGraphQL plugin.
With those plugins active, you can visit the GraphQL IDE located in your left hand admin menu.
This allows you to build queries and see the results right from your WordPress admin. For example, check products, then nodes, then name, and you will get a list of your products by name.
Nodes and Edges
The GraphQL schema uses a unique structure. You’ll see “nodes” and “edges” a lot in your queries, what are they exactly?
This image from the Apollo website explains it well. Nodes are the circles, and edges are the lines.
In WooCommerce, a node would be a product, and you can find related products on the edges. This will become more clear when we look at some code.
Getting Products
You can construct simple queries in your GraphQL IDE, so I won’t rehash that here. I think it’s more useful to see the query in use in an app. Here’s the full code I use to fetch products, using Apollo and React.
import React from "react";
import { useQuery, gql } from '@apollo/client';
const PRODUCTS = gql`
query GetProducts($first: Int, $last: Int, $after: String, $before: String) {
products(first: $first, last: $last, after: $after, before: $before) {
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
edges {
cursor
node {
id
slug
name
type
databaseId
shortDescription
image {
id
sourceUrl
altText
}
galleryImages {
nodes {
id
sourceUrl
altText
}
}
... on SimpleProduct {
onSale
price
content
regularPrice
}
... on VariableProduct {
onSale
price
content
regularPrice
}
}
}
}
}
`;
const ProductTab: React.FC = () => {
const variables = {
first: 10,
last: null,
after: null,
before: null
};
const { loading, error, data, fetchMore, refetch, networkStatus } = useQuery(PRODUCTS, { variables });
if (loading) return <Loading />;
if (error) {
console.warn(error)
return <p>Error</p>;
}
// Function to update the query with the new results
const handleUpdateQuery = (previousResult: { products: { edges: any; }; }, { fetchMoreResult }: any) => {
setDisableInfiniteScroll(true);
if( !fetchMoreResult || !fetchMoreResult.products.edges.length ) return previousResult;
fetchMoreResult.products.edges = [ ...previousResult.products.edges, ...fetchMoreResult.products.edges ]
return fetchMoreResult;
};
const loadMore = () => {
fetchMore({
variables: {
first: null,
after: data?.products?.pageInfo?.endCursor || null,
last: null,
before: null
},
updateQuery: handleUpdateQuery
});
}
const products = data?.products?.edges || [];
return (
<div>
<ul>
{ products && products.map( product => ( <li key={product.node.id />{ product.node.name }</li> }
</ul>
</div>
);
};
export default ProductTab;
Getting a Single Product
In my app, I send the product details to the single product page using history. That way I don’t have to fetch the product again, and the product loads instantly.
history.push('/products/' + product.databaseId, { product: product } );
Another way to do this would be to get less fields on the product list page, and then fetch the other fields you need when you get to the single product page.
Here’s an example query for a product by ID, that also fetches related products.
query MyQuery {
product(id: $id) {
name
id
databaseId
date
related {
edges {
node {
id
name
date
}
}
}
}
}
Keep in mind that the ID used to fetch the product is the GraphQL id, not the database id. It looks something like “cHJvZHVjdDo0MQ==”.
You can also query fields by product type. For example, on a variable product, we want to get the product attributes.
query MyQuery {
product(id: $id ) {
name
id
databaseId
date
related {
edges {
node {
id
name
}
}
}
... on VariableProduct {
id
name
attributes {
nodes {
name
}
}
}
}
}
I made a separate component for related products that I can add to my single product page. Here is the full code for that.
import React from "react";
import { useQuery, gql } from '@apollo/client';
const RELATEDPRODUCTS = gql`
query getProduct($id: ID! ) {
product(id: $id) {
related {
edges {
node {
id
slug
name
type
databaseId
shortDescription
image {
id
sourceUrl
altText
}
galleryImages {
nodes {
id
sourceUrl
altText
}
}
... on SimpleProduct {
onSale
price
content
regularPrice
}
... on VariableProduct {
onSale
price
content
regularPrice
}
}
}
}
}
}
`;
const RelatedProducts:React.FC<{ id: number | string }> = ( { id } ) => {
const { data, loading, error } = useQuery( RELATEDPRODUCTS, { variables: { id: id } } );
const history = useHistory();
if( loading ) {
return <></>;
}
if( error ) {
console.warn(error);
}
const go = product => {
history.push('/products/' + product.databaseId, { product: product } );
}
const products = data?.product?.related?.edges || [];
return (
<div>
{ data && products.map( ( product ) => (
<div key={product.node.id}>
<button onClick={ () => { go(product.node) }}>
{ product.node.image && <div className="related-image-wrap" style={{ backgroundImage: `url(${product.node.image.sourceUrl})` }}></div> }
<p>{ product.node.name }</p>
</button>
</div>
))}
</div>
)
}
export default RelatedProducts;
Cart
In this app the cart needs to be saved locally to the app, but we also need to add items to the cart in WooCommerce. We can do that with WooGraphQL by using a mutation.
Here’s my add to cart button component.
import { useMutation, gql } from "@apollo/client";
import React, { useContext } from 'react';
import CartContext from '../context/cart-context'
const AddToCart: React.FC<any> = ( { product } ) => {
const [ addToCartMutation ] = useMutation( ADD_TO_CART );
const {items, addToCart, removeFromCart} = useContext(CartContext);
function handleAddToCart() {
let id = product.databaseId;
// add to cart on server
addToCartMutation( { variables: { input: { productId: id, quantity: 1, clientMutationId: '123' } } } );
// save locally
addToCart( product )
}
return (
<button
onClick={ () => handleAddToCart() }
>Add to Cart</button>
)
}
const ADD_TO_CART = gql`
mutation ATC($input: AddToCartInput!) {
addToCart(input: $input) {
cart {
subtotal
total
shippingTotal
contents {
itemCount
nodes {
product {
node {
name
sku
databaseId
... on SimpleProduct {
price
}
}
}
}
}
}
}
}
`
export default AddToCart;
You’ll notice I’m also saving the product in global state after adding it to the cart. This is because we will display a cart page that is local to the app, even though checkout is on the normal site.
Checkout
When we go to checkout, we are going to use the actual website page, not a headless checkout. It’s possible to build a headless checkout, but then you need to handle all the code for payment gateways, taxes, coupons, shipping, etc. in the app. If you are using only Stripe, this is not a problem. For most projects this is an insurmountable task, so we will send people to the WooCommerce checkout page.
To accomplish this, we will use a technique I learned from this post by Jacob Arriola.
First, we save a customer’s session ID when adding to cart using Apollo Client. Here is example code for that.
After that, we need to grab the session ID using a JWT decode and then add that to a url parameter. Here’s my checkout button component.
import React, { useState } from 'react'
import jwtDecode from 'jwt-decode'
const CheckoutButton:React.FC = () => {
const session = useState( () => {
const jwtSession = window.localStorage.getItem('woo-session');
if( !jwtSession ) return null;
try {
const decoded = jwtDecode<{ data: { customer_id: string } }>(jwtSession);
return decoded.data.customer_id;
} catch( err ) {
console.warn(err);
return null;
}
} )
const checkoutLink = () => {
window.open(`https://test1.reactordev.com/checkout?session_id=${session[0]}`)
}
return(
<button onClick={ () => checkoutLink() }>Checkout</button>
)
}
export default CheckoutButton;
The last thing to do is add some custom plugin code to our WordPress site that handles loading the session from our url parameter. See Jacob’s post to get that code.
Now the app will load the checkout page. (Some custom CSS would make this look more seamless)