Ganesh Mani I'm a full-stack developer, Android application/game developer, and tech enthusiast who loves to work with current technologies in web, mobile, the IoT, machine learning, and data science.

Building an online store with Saleor

6 min read 1820

The Saleor logo.

In the era of modern web development, headless solutions become a popular way to build any kind of product. For example, Content Management systems, e-commerce solutions etc. In this article, we are going to see how to build an online store using headless e-commerce product Saleor.io, which is popular in the industry.

What is Saleor.io?

Saleor is a headless GraphQL ECommerce platform which helps business owners and development teams to build products faster in a shorter span of time. It provides us with all the modules we need to run an e-commerce store in a scalable way.

Saleor architecture

Saleor contains three important modules, including a backend solution and infrastructure, an admin dashboard for an e-commerce store, and an e-commerce client side solution.

A flowchart showinghow Saleor works.

Our store ( or client) communicates with backend through the Saleor SDK. At the same time, the admin dashboard communicates with the backend using a GraphQL API.

In this tutorial, we will see how to setup a Saleor backend and Admin dashboard for an e-commerce store. We will also see how to build a storefront (client) side for an e-commerce application.

Installation and setup

Note: to set up and run Saleor in a development environment, it’s best to use Docker.

We are going to use Docker for setting it up in our development environment.

git clone https://github.com/mirumee/saleor-platform.git --recursive --jobs 3
cd saleor-platform
docker-compose build

With the above command, we’ll build Docker images for us in our local machine.

docker-compose run --rm api python3 manage.py migrate
docker-compose run --rm api python3 manage.py collectstatic --noinput

Since the Saleor server is running in Python, it sets up the server and database migration for us.

//first command is optional
docker-compose run --rm api python3 manage.py populatedb 
docker-compose run --rm api python3 manage.py createsuperuser

After that, we populate our database with dummy data (this is optional), and then we create a super user for our admin dashboard.

Finally, to run the services, use this command:

docker-compose up

Now, if you visit the URL, you’ll see this:

http://localhost:3000/ – Client side
http://localhost:8000/graphql/ – GraphQL playground
http://localhost:9000/ – Admin Dashboard

Information about the store.

Our storefront.

Building a sticker store using Saleor

Now, we have the basic Saleor setup to build an online store. Let’s build a developer stickers store by customizing the Saleor storefront.



Devops.

Here are all the important modules/functionalities that we will be focusing on:

  • Listing Products based on category
  • Featured product
  • Search product
  • User – Login/Signup
  • Add to cart
  • Checkout product
  • Payment

To get the data from our GraphQL server, we need ApolloClient. This is because @saleor/sdk uses Apollo internally.

Let’s set up an Apollo Client in our React application.

npm install @saleor/sdk
import { SaleorProvider, useAuth } from "@saleor/sdk";

const config = { apiUrl: "http://localhost:8000/graphql/" };
const apolloConfig = {
  /* 
    Optional custom Apollo client config.
    Here you may append custom Apollo cache, links or the whole client. 
    You may also use import { createSaleorCache, createSaleorClient, createSaleorLinks } from "@saleor/sdk" to create semi-custom implementation of Apollo.
  */
};

const rootElement = document.getElementById("root");
ReactDOM.render(
  <SaleorProvider config={config} apolloConfig={apolloConfig}>
    <App />
  </SaleorProvider>,
  rootElement
);

const App = () => {
  const { authenticated, user, signIn } = useAuth();

  const handleSignIn = async () => {
    const { data, dataError } = await signIn("[email protected]", "admin");

    if (dataError) {
      /**
       * Unable to sign in.
       **/
    } else if (data) {
      /**
       * User signed in succesfully.
       **/
    }
  };

  if (authenticated && user) {
    return <span>Signed in as {user.firstName}</span>;
  } else {
    return <button onClick={handleSignIn}>Sign in</button>;
  }
};

Now, we need to build components and use the GraphQL Server to list and update the data to our server.

@saleor/sdk provides a lot of custom Hooks out of the box. It helps us to save lot of development time to build the store. A few of them are:

  • useAuth – Authentication hooks which handles authentication mechanism. it provides the logged in user info and check if the user is authentication etc.
  • useCart – it provides functionality to add product to cart and remove product from cart.
  • useSignOut – it sign out the user from the store.
  • useUserDetails – it returns the information about the logged in user.
  • useUserDetails – it provides functionality to checkout all the items in the cart.

There are lot of custom Hooks like this. I am just pointing out some of the important ones here. You can check all the Hooks in the @saleor/sdk.

SDK React.

Now, we are going to fetch the products and product category from the server. But, if you skipped the step of populating the database with dummy data on the installation, you may need to add Products and Categories to the database.

We can do that using our store admin dashboard. Go to Catalog => Product in the admin dashboard and add the products.

Catalog.

Do the same for Categories as well. This way, you can have Categories and Products to fetch from the server.

Once you add both of them, we can write GraphQL queries to fetch the product and categories.

query ProductsList {
    products(first: 5) {
      edges {
        node {
          id
          name
          pricing{
            onSale
            discount{
              gross{
                amount
              }
              currency
            }
          }
          description
          category{
            id
            name
          }
          images{
            id
            url
          }
        }
      }
    }
    categories(level: 0, first: 4) {
      edges {
        node {
          id
          name
          backgroundImage {
            url
          }
        }
      }
    }
  }

It fetches both products and categories from the server.

Product list.

You can use that data to list all the products and categories in our store.

import React, { useRef, useState } from "react";
import {
  ProductsList_categories,
  ProductsList_shop,
  ProductsList_shop_homepageCollection_backgroundImage,
} from "./gqlTypes/ProductsList";
import { structuredData } from "../../core/SEO/Homepage/structuredData";
// import noPhotoImg from "../../images/no-photo.svg";
import ProductItem from '../../components/product/ProductItem'
import Boundary from './boundary'
const Page = ({ loading, categories, products, backgroundImage, shop }) => {
  const [columnCount, setColumnCount] = useState(10);
  const categoriesExist = () => {
    return categories && categories.edges && categories.edges.length > 0;
  };
  const productListWrapper = useRef(null);
  return (
    <>
      <script className="structured-data-list" type="application/ld+json">
        {structuredData(shop)}
      </script>
      <main className="content">
        <section className="product-list-wrapper">
          <Boundary>
            <div
              className="product-list"
              ref={productListWrapper}
              style={{ gridTemplateColumns: `repeat(${columnCount}, 160px)` }}
            >
              {products.edges.map((product, index) => {
                return (
                  <ProductItem
                    foundOnBasket={false}
                    product={product.node}
                    key={`product-skeleton ${index}`}
                  />
                )
              })}
            </div>
          </Boundary>
        </section>
      </main>
    </>
  );
};
export default Page;

So far, we have the product and category listing functionalities. Now, we need to implement a way to add the product into a basket and add user login/signup functionalities.

Add to cart functionality

Devops.

When a user clicks the Add to cart button, we use the @saleor/sdk custom Hook useCart to add the item to their cart.

import React from 'react';
import { displayMoney } from './utils';
import { useCart } from "@sdk/react";
const ProductItem = ({
    product,
    onOpenModal,
    displaySelected,
    foundOnBasket
}) => {
    const { addItem, removeItem } = useCart()
    const onClickItem = () => {
        if (product.id) {
            onOpenModal();
            displaySelected(product);
        }
    };
    const onAddToBasket = () => {
        addItem(product.id, 1)
    };
    return (
        <div
            className={`product-card ${!product.id ? 'product-loading' : ''}`}
            style={{
                border: foundOnBasket ? '1px solid #cacaca' : '',
                boxShadow: foundOnBasket ? '0 10px 15px rgba(0, 0, 0, .07)' : 'none'
            }}
        >
            {foundOnBasket && <i className="fa fa-check product-card-check" />}
            <div
                className="product-card-content"
                onClick={onClickItem}
            >
                <div className="product-card-img-wrapper">
                    {product.images[0].url ? (
                        <img
                            className="product-card-img"
                            src={product.images[0].url}
                        />
                    ) : null}
                </div>
                <div className="product-details">
                    <h5 className="product-card-name text-overflow-ellipsis margin-auto">{product.name || null}</h5>
                    <p className="product-card-brand">{product.brand || null}</p>
                    <h4 className="product-card-price">{product.price ? displayMoney(product.price) : null}</h4>
                </div>
            </div>
            {product.id && (
                <button
                    className={`product-card-button button-small button button-block ${foundOnBasket ? 'button-border button-border-gray' : ''}`}
                    onClick={onAddToBasket}
                >
                    {foundOnBasket ? 'Remove from basket' : 'Add to basket'}
                </button>
            )}
        </div>
    );
};
export default ProductItem;

Here, we implement the custom Hook useCart. Using that, we can add items to the cart and remove items from the cart.

const { addItem, removeItem } = useCart()

Display cart

To get all the items from the cart, you can use the same custom Hooks:

const {
    items,
    removeItem,
    subtotalPrice,
    shippingPrice,
    discount,
    totalPrice,
  } = useCart();

It returns all the the items to us that were added to cart. Now, we can use the items to show it in our e-commerce store.

T shirt.

User login/signup

To implement the log in functionality, it’s pretty straightforward. We need to use the custom Hook useSignIn from the @saleor/sdk, which returns the data after a successful login.

import "./scss/index.scss";
import * as React from "react";
import { useSignIn } from "@sdk/react";
import { maybe } from "@utils/misc";
import { Button, Form, TextField } from "..";
const LoginForm = ({ hide }) => {
  const [signIn, { loading, error }] = useSignIn();
  const handleOnSubmit = async (evt, { email, password }) => {
    evt.preventDefault();
    const authenticated = await signIn({ email, password });
    if (authenticated && hide) {
      hide();
    }
  };
  return (
    <div className="login-form">
      <Form
        errors={maybe(() => error.extraInfo.userInputErrors, [])}
        onSubmit={handleOnSubmit}
      >
        <TextField
          name="email"
          autoComplete="email"
          label="Email Address"
          type="email"
          required
        />
        <TextField
          name="password"
          autoComplete="password"
          label="Password"
          type="password"
          required
        />
        <div className="login-form__button">
          <Button type="submit" {...(loading && { disabled: true })}>
            {loading ? "Loading" : "Sign in"}
          </Button>
        </div>
      </Form>
    </div>
  );
};
export default LoginForm;

User signup is a bit different from the login functionality. We need to use a GraphQL mutation, which is:

mutation RegisterAccount(
    $email: String!
    $password: String!
    $redirectUrl: String!
  ) {
    accountRegister(
      input: { email: $email, password: $password, redirectUrl: $redirectUrl }
    ) {
      errors {
        field
        message
      }
      requiresConfirmation
    }
  }

When the user signs up successfully, it redirects them to the specified URL.

Implementing cart checkout

Cart checkout involves 4 steps to complete in terms of functionality. They are:

  • Getting Address from the user
  • Payment
  • Confirmation
  • Shipping method configuration

Shipping address.

Similar to the other functionalities, we can use a custom Hook, useCheckout, which provides all the functionality needed for checkout.

Getting the address from the user

We need to get the address from the user. You can either ask them to add a new one, or select an existing address. To do that, we can use the function from the useCheckout:

const {
    checkout,
    setShippingAddress,
    selectedShippingAddressId,
  } = useCheckout();

Payment

To make a payment, Saleor provides us functions such as:

const {
    checkout,
    billingAsShipping,
    setBillingAddress,
    setBillingAsShippingAddress,
    selectedBillingAddressId,
    availablePaymentGateways,
    promoCodeDiscount,
    addPromoCode,
    removePromoCode,
    createPayment,
  } = useCheckout();

Using the createPayment function, we can complete the transaction for the order.

const handleProcessPayment = async (
    gateway,
    token,
    cardData
  ) => {
    const { dataError } = await createPayment(gateway, token, cardData);
    const errors = dataError?.error;
    changeSubmitProgress(false);
    if (errors) {
      setGatewayErrors(errors);
    } else {
      setGatewayErrors([]);
      history.push(CHECKOUT_STEPS[2].nextStepLink);
    }
  };

Confirmation/Review

This is just for user to review and verify that everything is correct so far. In this step, you can complete the checkout using this function:

const { checkout, payment, completeCheckout } = useCheckout();

Conclusion

To summarize, Saleor is a powerful GraphQL headless for e-commerce development. It provides all the required functionalities that an SDK provides. As a result, we can just focus on the UI for our e-commerce store. It helps us to avoid the complex setup of the backend admin panel.

LogRocket: See the technical and UX reasons for why users don’t complete a step in your ecommerce flow.

LogRocket is like a DVR for web and mobile apps and websites, recording literally everything that happens on your ecommerce app. Instead of guessing why users don’t convert, LogRocket proactively surfaces the root cause of issues that are preventing conversion in your funnel, such as JavaScript errors or dead clicks. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Start proactively monitoring your ecommerce apps — .

Ganesh Mani I'm a full-stack developer, Android application/game developer, and tech enthusiast who loves to work with current technologies in web, mobile, the IoT, machine learning, and data science.

Leave a Reply