Creating a SPA with Gatsby

Gatsby is a static site generator with server side rendering. In practice, this means that your sites will get built on a CICD box somewhere. Your site will have static routes with static pages like / and /blog/ etc.

So, what happens when you realize you need SPA functionality, as we did?

Background context of our problem

When we started out with GMGT, it was going to be a site with a blog, cms, and storefront. Simple enough. However, a month later the request for playing audio while navigating around came-in. Very doable with a single page application, but much harder when you’re generating static pages as the audio will bounce around after each redirect.

How did we solve it?

I came up with 3 approaches:

  1. Hack on graphql

  2. Put our content into firestore

  3. Redirect magic

Put our content into firestore

It kind of works… I created a React hook to make a request with axios:

  useEffect(() => {
    if (!props?.location?.state?.product) {
      console.error('making db request');
      axios.get(`http://localhost:5001/goodmusicgoodtimes-8cbb2/us-central1/getContent?id=${props.productId}`)
        .then((response) => {
          setData(response.data.node.frontmatter);
        }).catch((err) => {
          console.error(err);
        });
      }
  }, [])

However, the images get busted. They’re no longer objects but only URLs after serialization. What else can we do?

Reach router magic

With Reach router, you can pass props through a redirect.

So I tried that: <ProductListingItemLink to={product.id} state={{ product }}>

Unfortunately, this means that navigating directly to the product (ex: /product/product-1) would fail as nothing was there from the props!

Filtering all pages

This was the option that I initially considered… but wrote off due to performance concerns. I tried several things with this, like string interpolation of the graphQL query, which failed by design (hello SQL injection my old friend). But after looking at other options, using GraphQL seemed like the right way to go.

So I needed hard evidence to test my assumptions on performance.

Given something like:

products: allMarkdownRemark(filter: {frontmatter: {templateKey: {eq: "product-page"}}}) {
...
  products.edges.forEach((edge) => {
    if (edge.node.id == props.productId) product = edge.node;
  });

How would it perform? Here were my findings:

  • all content query: 0.147216796875ms with 72 product pages (no images)
  • Typical REST request is 30ms+
  • Requires stub for compile time

The stubbing is annoying but not a deal breaker. Images will probably be the true breaking point.

Final solution:

export const RuntimeProduct = (props) => {
  const { products } = useStaticQuery(
    graphql`
          query AllProducts {
              products: allMarkdownRemark(filter: {frontmatter: {templateKey: {eq: "product-page"}}}) {
              edges {
                  node {
                  fields {
                      slug
                  }
                  id
                  frontmatter {
                      productId
                      title
                      description
                      price
                      image {
                            childImageSharp {
                            fluid(maxWidth: 1000, maxHeight: 1000) {
                                ...GatsbyImageSharpFluid
                            }
                            }
                        }
                      skuInfo {
                          skuId
                          attributes {
                              key
                              value
                          }
                      }
                      }
                  }
              }
          }
      }
      `);
  let product = {
    frontmatter: {
      title: '',
      description: '',
      price: '',
      productId: "",
      skuInfo: [],
      image: {
        childImageSharp: {
          fluid: ''
        }
      },
    }
  };
  products.edges.forEach((edge) => {
    if (edge.node.id === props.productId) product = edge.node;
  });
  return (
    <ProductPageTemplate
      {...product.frontmatter}
    />
  )
}

Whether or not it holds up against time remains to be seen.