April 03, 2022

Build full stack e-commerce web app with React/Next.js , Apollo/GraphQL and Keystone Headless CMS - Part 2

Welcome to this second part of Build full-stack E-commerce web app with React/Next.js , Apollo/GraphQL and Keystone Headless CMS.

In this part,we will be focusing mainly on connecting the backend from our frontend app. We will be using apollo client, a management library for JavaScript that will enable us to manage data with GraphQL.

Switch back to our client folder and install the below packages:-

npm install @apollo/client graphql apollo-link-error

@apollo/client include n-memory cache, local state management, error handling, and a React-based view layer while graphql package will give us logic to parse our GraphQL logic queries.

apollo-link-error will allow us to do some custom logic when a GraphQL or network error happens hence easy troubleshooting.

Once installed, the first thing is to initialize apollo client. Since we already have a GraphQL server created with our keystonejs backend, we will include it in the configuration.

We will also connect our application to apollo client using ApolloProvider. This is done by wrapping our Main Component with ApolloProvider component which will pass client props as below:-

import "../styles/globals.css";
import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  ApolloLink,
  HttpLink,
} from "@apollo/client";
import { onError } from "apollo-link-error";

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: ApolloLink.from([
    onError(({ graphQLErrors, networkError }) => {
      if (graphQLErrors)
        graphQLErrors.forEach(({ message, locations, path }) =>
          console.log(
            `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
          )
        );
      if (networkError)
        console.log(
          `[Network error]: ${networkError}. Backend is unreachable. Is it running?`
        );
    }),
    new HttpLink({ uri: "http://localhost:5000/api/graphql" }),
  ]),
});

function MyApp({ Component, pageProps }) {
  return (
    <ApolloProvider client={client}>
      <Component {...pageProps} />
    </ApolloProvider>
  );
}

export default MyApp;

From here, if we relaunch our application, we should expect to see our graphql api being called with CORS error. TO resolve the cors error, let’s go back to our keystonejs config file (keystone.ts) under server and allow cors from our client url as below:

  server: {
      port: 5000,
      cors: {
        origin: [process.env.FRONTEND_URL],
        credentials: true,
      },
    },

Now, inspecting again, you should see the request success with 200 response, with data in it.

The next step is to display our list of products in our homepage. Under pages, inside inde.js file, we will first need to define our query as below :-

import { gql } from "@apollo/client";

const GET_PRODUCTS = gql`
    query getProducts {
      products {
        name
        price
      }
    }
  `;

the imported gql function above will parse our string into query document.

Once we have defined our query, we will then use react hook called useQuery to get our data. useQuery will return three information, the loading state, errors if any as well as our data. Go ahead and add as below:-

const { loading, error, data } = useQuery(PRODUCTS_QUERY);

For now we will try to console log error and data and see what we get. Our index.js file should be as below :-

import Head from "next/head";
import Header from "../components/Header/Header";
import styles from "../styles/Home.module.css";
import { useQuery, gql } from "@apollo/client";

export default function Home() {
  const PRODUCTS_QUERY = gql`
    query getProducts {
      products {
        name
        price
      }
    }
  `;
  const { loading, error, data } = useQuery(PRODUCTS_QUERY);
  console.log("Error below");
  console.log(error);
  console.log("Data below");
  console.log(data);

  return (
    <div className={styles.container}>
      <Head>
        <title>Buymine - your next shopping partner</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Header />
      <main className={styles.main}>
        <h1 className={styles.title}>Welcome to Buymine</h1>
      </main>

      <footer className={styles.footer}>
        Copyright &copy; 2022. All rights reserved
      </footer>
    </div>
  );
}

Now if everything goes well, you should be able to get the list of products with name and price as specified in our query and error should be undefined if none.

Now that we are getting our products, let’s loop through them and display in our product component. Go ahead under components, create a folder called Product and inside it, product.js and product.css as below:-

Product.css

.product {
  flex-direction: column;
  display: flex;
  flex: 1 0 21%;
  margin: 5px;
  justify-content: center;
  align-items: center;
  margin-bottom: 15px;
  cursor: pointer;
  text-align: center;
}
.productImage {
  width: 240px;
  height: 240px;
  object-fit: contain;
  margin-bottom: 15px;
}
.productTitle {
  max-height: 37px;
  font-size: 0.875rem;
  color: rgb(17, 24, 32);
  text-align: center;
  font-weight: normal;
  margin-top: 5px;
}
.productPrice {
  font-size: 14px;
  margin: 0;
  font-weight: bolder;
}

Product.js

import styles from "./Product.module.css";
import Link from "next/link";

const Product = ({ product }) => {
  return (
    <div className={styles.product}>
      <Link href={`product/${product.id}`}>
        <a>
          <img
            src={product.image.publicUrlTransformed}
            alt={product.name}
            className={styles.productImage}
          />
          <h3 className={styles.productPrice}>$ {product.price}</h3>
          <h2 className={styles.productTitle}>{product.name}</h2>
        </a>
      </Link>
    </div>
  );
};
export default Product;

The Product component will accept a product prop that will contain our product information.

We will use a Link component to allow client side transitions between pages. In our case, we will need to handle dynamic routing as we are passing product id on our link, more on that later as we do a single product page.

Since we already have our products,let’s looop through them and display them using our newly created Product component.

import Head from "next/head";
import Header from "../components/Header/Header";
import styles from "../styles/Home.module.css";
import { useQuery, gql } from "@apollo/client";
import Product from "../components/Product/Product";

export default function Home() {
  const PRODUCTS_QUERY = gql`
    query getProducts {
      products {
        id
        name
        price
        description
        image {
          publicUrlTransformed
        }
      }
    }
  `;
  const { loading, error, data } = useQuery(PRODUCTS_QUERY);

  return (
    <div className={styles.container}>
      <Head>
        <title>Buymine - your next shopping partner</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Header />
      <main className={styles.main}>
        {!loading &&
          data.products.map((product) => (
            <Product product={product} key={product.id} />
          ))}
      </main>

      <footer className={styles.footer}>
        Copyright &copy; 2022. All rights reserved
      </footer>
    </div>
  );
}

Let’s finalize our pages by adding single product page.Note with single product page we have to do dynamic routing passing productId in our url as /product/productId.

To achieve the above, inside pages folder, we need to create a new folder called product and then inside product [productId].js file.

With the above, any route to /product/[productId] will be matched by pages/product/[productId].js. The matched path parameter will be sent as a query parameter to the page, and it will be merged with the other query parameters.

[productId].js

import { useRouter } from "next/router";
import styles from "./ProductInfo.module.css";
import { useQuery, gql } from "@apollo/client";
import { useEffect, useState } from "react";

const ProductInfo = () => {
  const router = useRouter();
  const { productId } = router.query;
  console.log("router.query = ", router.query);
  const [product, setProduct] = useState(null);

  const SINGLE_PRODUCT_QUERY = gql`
  query getProduct {
    products(where:{id:{equals:"${productId}"}}){
        id
        name
        price
        description
        image {
          publicUrlTransformed

      }
    }
  }
  `;
  const { loading, error, data } = useQuery(SINGLE_PRODUCT_QUERY);
  useEffect(() => {
    if (!loading) {
      setProduct(data.products[0]);
    }
  }, [loading]);

  return (
    <div className={styles.productInfo}>
      {product && (
        <>
          <div className={styles.productInfo__image}>
            <img
              src={product.image.publicUrlTransformed}
              alt={product.name}
              className={styles.productImage}
            />
          </div>
          <div className={styles.productInfo__desc}>
            <h1>{product.name}</h1>
            <h2 className={styles.productInfo__price}>${product.price}</h2>

            <div>{product.description}</div>
          </div>
        </>
      )}
    </div>
  );
};

export default ProductInfo;

From the above, we created a SINGLEPRODUCTQUERY which filters the product by productId. We then use useQuery to fetch and display the results, straight forward, right :-)

Also don’t forget to add the css below inisde pages/product

ProductIndfo.css

.productInfo {
  display: flex;
  margin-top: 20px;
  margin-right: 55px;
}
.productInfo__image {
  display: flex;
  flex-direction: column;
}

.productInfo div.productInfo__image {
  flex: 1 0 30%;
  padding: 0px 10px;
  justify-content: center;
  align-items: center;
}
.productInfo div.productInfo__image img {
  width: 300px;
  height: auto;
}
.productInfo div.productInfo__desc {
  flex: 1 0 70%;
  padding: 10px 15px;
  box-shadow: 0 1px 2px 1px rgb(0 0 0 / 15%);
}
.productDesc {
  margin: 5px;
}
.productInfo h1,
.productInfo h2 {
  margin: 10px 0px;
}
.productInfo h5 {
  margin-top: 0px;
  margin-bottom: 10px;
}

And that’s that, when you go back to home page and click a product you should be able to reach the product details page.

One more thing before we end this part. Notice when we go to product Info page we don’t have a header and footer? Okay, let’s fix that quickly by creating a layout.

We can easily achieve that by modifying our custom App componnet by wrapping it with Layout component as below:-

function MyApp({ Component, pageProps }) {
  return (
    <ApolloProvider client={client}>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </ApolloProvider>
  );
}

Let’s go ahead and create a Layout component. Inside coponents folder, create a Layout.js file as below:-

import Head from "next/head";
import Header from "./Header/Header";
import styles from "../styles/Home.module.css";

const Layout = ({ children }) => {
  return (
    <>
      <Head>
        <title>Buymine - your next shopping partner</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Header />
      {children}

      <footer className={styles.footer}>
        Copyright &copy; 2022. All rights reserved
      </footer>
    </>
  );
};

export default Layout;

Now if we go ahead and remove Header and footer code in our index.js, we can now see they will show up in both pages.

Our index.js file should now be as below : -

import styles from "../styles/Home.module.css";
import { useQuery, gql } from "@apollo/client";
import Product from "../components/Product/Product";

export default function Home() {
  const PRODUCTS_QUERY = gql`
    query getProducts {
      products {
        id
        name
        price
        description
        image {
          publicUrlTransformed
        }
      }
    }
  `;
  const { loading, error, data } = useQuery(PRODUCTS_QUERY);

  if (error) return <div>Failed to load</div>
  if (!data) return <div>Loading...</div>


  return (
    <div className={styles.container}>
      <main className={styles.main}>
        {!loading &&
          data.products.map((product) => (
            <Product product={product} key={product.id} />
          ))}
      </main>
    </div>
  );
}

And that’s it for now. On the next, we will be exploring other functionalities like adding items to cart and more. see you then.


Robert Rutenge
Front-end Developer | ReactJS enthusiast | Problem Solver . Find me on twitter and Github