Architecture Overview

Fetching Products

In Saleor, you can fetch:

  • a collection of products by using the products query, or
  • a single product by using the product query that requires the product ID as input.

The first operation also enables you to filter and sort a collection of products by specifying either the filter criteria or the sorting parameters.

Fetching a collection

Let's start with a simple collection of 12 elements that returns a product id and name. Put the following GraphQL query inside the GraphQL Playground of your Saleor endpoint and execute it using the play button in the middle.

# graphql/queries/ProductGetTwelveElements.graphql
query ProductGetTwelveElements {
  products(first: 12, channel: "default-channel") {
    edges {
      node {
        id
        name
      }
    }
  }
}

Heads up!

It's a good practice to always name your GraphQL queries even if it's not required by the GraphQL engine as it helps with caching and debugging.

The first parameter specifies how many elements we want to fetch from the Saleor API. This parameter is required to avoid fetching too much data in case we have many products in our backend.

Here's how a typical response to that query may look like:

{
  "data": {
    "products": {
      "edges": [
        {
          "node": {
            "id": "UHJvZHVjdDo3Mg==",
            "name": "Apple Juice"
          }
        },
        {
          "node": {
            "id": "UHJvZHVjdDo3NA==",
            "name": "Banana Juice"
          }
        }
        // ... 10 more
      ]
    }
  }
}

Another way of controlling the number of elements to fetch is through pagination. We will discuss it at the end of this section.

Autogenerate a React Hook for Products

  1. Put the abovementioned query in your storefront under the graphql/queries directory as ProductGetTwelveElements.graphql.

  2. In the Terminal, run the generate script to generate the corresponding React.js hooks:

npm run generate

or

pnpm run generate

Heads up!

If you haven't already, you can add the -w option at the end of the generate script in the package.json file which we configured in the setup section. This option instructs the code generation to keep running and watch for changes in GraphQL files. You can leave it running in a terminal for convenience.

React Component for Product Collection

Let's create our first React component for displaying available products as a grid.

  1. Create the components folder in the root folder of your storefront.
  2. Create a file called ProductCollection.tsx there and copy/paste the code below:
// components/ProductCollection.tsx
import React from 'react';
import { useProductGetTwelveElementsQuery } from '@/saleor/api';
const styles = { grid: 'grid gap-4 grid-cols-4', product: { card: 'bg-white border', summary: 'px-4 py-2 border-gray-100 bg-gray-50 border-t', title: 'block text-lg text-gray-900 truncate', } } export const ProductCollection = () => {
const { loading, error, data } = useProductGetTwelveElementsQuery();
if (loading) return <p>Loading...</p>; if (error) return <p>Error</p>; if (data) { const products = data.products?.edges || []; return ( <ul className={styles.grid}> {products?.length > 0 && products.map( ({ node: { id, name } }) => ( <li key={id} className={styles.product.card}> <a> <div className={styles.product.summary}> <p className={styles.product.title}>{name}</p> </div> </a> </li> ), )} </ul> ); } return null; }

The GraphQL Code Generator alongside the Apollo React plugin automatically generates the useProductGetTwelveElementsQuery React hook.

Once the data is available, we display the product names as a list. It's worth noticing that the product collection exists as a GraphQL edge as it stores information that goes beyond what's in each object, e.g., the count. Each collection element is a node.

Let's define the export statement in components/index.ts for this component, so we can directly import it from @/components later on.

// components/index.ts
export { ProductCollection } from "./ProductCollection";

Display the Home Page

In Next.js, the routing is generated based on the file system. Each file in the pages directory is available as a route, and pages/index.tsx is the file associated with the / path. Right now, it's just a 4-element grid with links to Next.js resources. So, let's replace it with the ProductCollection component:

// pages/index.tsx
import React from "react";

import { ProductCollection } from "@/components";

const styles = {
  background: "min-h-screen bg-gray-100",
  container: "py-10 max-w-7xl mx-auto",
};

const Home = () => {
  return (
    <div className={styles.background}>
      <div className={styles.container}>
        <ProductCollection />
      </div>
    </div>
  );
};

export default Home;

As a result when navigating to localhost:3000/, you should see a grid of product names as shown below:

A grid of products.
A grid of products.

Additionally, we can standardize the structure for each Next.js page with a fixed layout.

  1. Create a Layout.tsx file in the components folder.
  2. Copy the code below and paste it into the Layout.tsx file:
// components/Layout.tsx
import React from "react";

interface Props {
  children: React.ReactNode;
}

const styles = {
  background: "min-h-screen bg-gray-100",
  container: "py-10 max-w-7xl mx-auto",
};

export const Layout = ({ children }: Props) => {
  return (
    <div className={styles.background}>
      <div className={styles.container}>{children}</div>
    </div>
  );
};
  1. Go to components/index.ts and define the export statement for this component.
// components/index.ts
export { ProductCollection } from './ProductCollection';
export { Layout } from './Layout';
  1. Rewrite the Home page in the following way:
// pages/index.tsx
import React from "react";

import { ProductCollection, Layout } from "@/components";

const Home = () => {
  return (
    <Layout>
      <ProductCollection />
    </Layout>
  );
};

export default Home;

Displaying product thumbnails

Right now, our product collection contains only the product names. Let's improve it by including the product images.

We need to slightly modify our GraphQL query to also include the product thumbnails in the response. While we are at it, we can also fetch the product category.

Here's the modified query:

# graphql/queries/ProductGetTwelveElements.graphql
query ProductGetTwelveElements {
  products(first: 12, channel: "default-channel") {
    edges {
      node {
        id
        name
thumbnail {
url
}
category {
name
}
} } } }

Let's use this query to replace the previous one:

  1. Paste the abovementioned query into the ProductGetTwelveElements.graphql that is located in graphql/.
  2. If you have enabled the watch mode in your generate script, the Code Generator will now regenerate the types and update hooks for the new query. If you haven't, run npm generate or pnpm generate in your Terminal to trigger the regeneration.
  3. Head over to the components folder and modify the ProductCollection.tsx component to display the product thumbnails along with their categories:
// components/ProductCollection.tsx
import React from "react";
import { useProductGetTwelveElementsQuery } from "@/saleor/api";

const styles = {
  grid: "grid gap-4 grid-cols-4",
  product: {
    card: "bg-white border",
    summary: "px-4 py-2 border-gray-100 bg-gray-50 border-t",
    title: "block text-lg text-gray-900 truncate",
    category: "block text-sm font-medium text-gray-500",
    image: {
      aspect: "aspect-h-1 aspect-w-1",
      content: "object-center object-cover",
    },
  },
};

export const ProductCollection = () => {
  const { loading, error, data } = useProductGetTwelveElementsQuery();

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error</p>;

  if (data) {
    const products = data.products?.edges || [];

    return (
      <ul role="list" className={styles.grid}>
        {products?.length > 0 &&
          products.map(({ node: { id, name, thumbnail, category } }) => (
            <li key={id} className={styles.product.card}>
              <a>
                <div className={styles.product.image.aspect}>
                  <img
                    src={thumbnail?.url}
                    alt=""
                    className={styles.product.image.content}
                  />
                </div>
                <div className={styles.product.summary}>
                  <p className={styles.product.title}>{name}</p>
                  <p className={styles.product.category}>{category?.name}</p>
                </div>
              </a>
            </li>
          ))}
      </ul>
    );
  }

  return null;
};
Products grid with thumbnails.
Products grid with thumbnails.

The Product Element component

Before we dive into other Saleor features, let's take a moment to slightly refactor the ProductCollection component. We can take each product element in the list and put it into a separate component.

  1. In the components/ folder create a ProductElement.tsx and copy/paste the code below:
// components/ProductElement.tsx
import React from "react";

const styles = {
  card: "bg-white border",
  summary: "px-4 py-2 border-gray-100 bg-gray-50 border-t",
  title: "block text-lg text-gray-900 truncate",
  category: "block text-sm font-medium text-gray-500",
  image: {
    aspect: "aspect-h-1 aspect-w-1",
    content: "object-center object-cover",
  },
};

import { Product } from "@/saleor/api";

type Props = Pick<Product, "id" | "name" | "thumbnail" | "category">;

export const ProductElement = ({ id, name, thumbnail, category }: Props) => {
  return (
    <li key={id} className={styles.card}>
      <a>
        <div className={styles.image.aspect}>
          <img src={thumbnail?.url} alt="" className={styles.image.content} />
        </div>
        <div className={styles.summary}>
          <p className={styles.title}>{name}</p>
          <p className={styles.category}>{category?.name}</p>
        </div>
      </a>
    </li>
  );
};
  1. Add the export statement for this component to components/index.ts:
// components/index.ts
export { ProductCollection } from './ProductCollection';
export { Layout } from './Layout';
export { ProductElement } from './ProductElement';
  1. Reorganize the ProductCollection component in the following way:
// components/ProductCollection.tsx
import React from "react";

import { Product, useProductGetTwelveElementsQuery } from "@/saleor/api";
import { ProductElement } from "@/components";

const styles = {
  grid: "grid gap-4 grid-cols-4",
};

export const ProductCollection = () => {
  const { loading, error, data } = useProductGetTwelveElementsQuery();

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error</p>;

  if (data) {
    const products = data.products?.edges || [];

    return (
      <ul role="list" className={styles.grid}>
        {products?.length > 0 &&
          products.map(({ node }) => (
            <ProductElement key={node.id} {...(node as Product)} />
          ))}
      </ul>
    );
  }

  return null;
};

Refactoring ProductElement out of ProductCollection doesn't change anything visually in our application at this stage. However, the code is organized a bit better.


Help us improve our docs. Edit this page on GitHub