How to Make a WooCommerce App with React and WPGraphQL

I recently made a simple eCommerce app using React, WPGraphQL, WooGraphQL, and the Ionic Framework.

React app with WPGraphQL and WooCommerce

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.

Source: Apollo

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)


Posted

in

by

Tags: