In Saleor, you can fetch:
products
query, orproduct
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.
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.
Put the abovementioned query in your storefront under the graphql/queries
directory as ProductGetTwelveElements.graphql
.
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.
Let's create our first React component for displaying available products as a grid.
components
folder in the root folder of your storefront.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";
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:
Additionally, we can standardize the structure for each Next.js page with a fixed layout.
Layout.tsx
file in the components
folder.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>
);
};
components/index.ts
and define the export statement for this component.// components/index.ts
export { ProductCollection } from './ProductCollection';
export { Layout } from './Layout';
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;
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:
ProductGetTwelveElements.graphql
that is located in graphql/
.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.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;
};
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.
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>
);
};
export
statement for this component to components/index.ts
:// components/index.ts
export { ProductCollection } from './ProductCollection';
export { Layout } from './Layout';
export { ProductElement } from './ProductElement';
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.