Back to Top

How to make Magento 2 cart page using ReactJS

Updated 30 September 2024

Learn how to build a Magento 2 cart page using ReactJs in a headless development environment.

1. Introduction

2. Setting Up Your Cart Page With ReactJs

3. Creating Your Cart Page with ReactJs

4. Creating Your Cart Page with ReactJs

Searching for an experienced
Magento 2 Company ?
Find out More

5. Conclusion

By leverage of Magento 2 and headless architecture, By integrating Magento 2 backend with React, allowing for a more flexible and dynamic user experience and the full potential of headless commerce.

Introduction

Creating a cart page in React for your Magento 2 store can significantly influence whether a user decides to buy your product or move on.

For product page setup you can check out our another blog on How To Create Magento 2 Product Page In Reactjs.

This guide will take you through the process of building an efficient and user-friendly cart page, featuring code examples and detailed explanations.

You’ll gain valuable insights into React.js development along the way.

You’ll learn how to fetch data from Magento 2 GraphQL API, manage state, and design a responsive layout that enhances the overall shopping experience.

Setting Up Your Cart Page with ReactJs

1. Create Your ReactJs Project:

Open your terminal and run the following command to create a new React.Js Project

npx create-react-app my-magento-store
cd my-magento-store

2. Navigate to root directory:

cd my-magento-store

3. Install Necessary Packages In Your ReactJs:

As, I created this with Tailwind, Eslint, React router dom, TypeScript so:

// typescript and eslint
npm install -D typescript eslint

// tailwind css
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

// react router
npm install react-router-dom

Creating Your Cart Page with ReactJs

1. Set Up GraphQL Queries:

As we are using Magento 2 GraphQl API. So lets, create a file for your GraphQL queries. This keeps your code organized.

mkdir src/components/cart
touch src/components/cart/queries.ts
export const GET_CART = `
    query ($cartId: String!) {
  cart(cart_id: $cartId) {
    id
    total_quantity
    applied_coupons {
      code
    }
    items {
      cartItemId: uid
      quantity
      product {
        name
        sku
        stock_status
        description {
          html
        }
        url_key
        name
        thumbnail {
          id: url
          url
          label
        }
      }

      prices {
        price {
          currency
          value
        }
        row_total {
          currency
          value
        }
        row_total_including_tax {
          currency
          value
        }
        total_item_discount {
          currency
          value
        }
        discounts {
          label
          amount {
            currency
            value
          }
        }
      }
    }
    shipping_addresses {
      country {
        code
        label
      }
      region {
        code
        region_id
        label
      }
      available_shipping_methods {
        amount {
          currency
          value
        }
        available
        carrier_code
        carrier_title
        error_message
        method_code
        method_title
      }
      selected_shipping_method {
        amount {
          currency
          value
        }
        carrier_title
        carrier_code
        method_code
      }
    }
    prices {
      applied_taxes {
        amount {
          currency
          value
        }
        label
      }
      discounts {
        amount {
          currency
          value
        }
        label
      }
      grand_total {
        currency
        value
      }
      subtotal_excluding_tax {
        currency
        value
      }
    }
  }
}
`;

export const CREATE_EMPTY_CART_MUTATION = `
  mutation {
    createEmptyCart
  }
`;

export const UPDATE_CART_MUTATION = `
      mutation UpdateCartItem($cartId: String!, $cartItems:[CartItemUpdateInput]!) {
        updateCartItems(input: { cart_id: $cartId, cart_items:$cartItems }) {
          cart {
            id
            items {
              id
              quantity
            }
          }
        }
      }
    `;

export const DELETE_CART_MUTATION = `
    mutation DeleteCartItem($cartId: String!, $itemId: ID!) {
      removeItemFromCart(input: { cart_id: $cartId, cart_item_uid: $itemId }) {
        cart {
          id
          items {
            id
            quantity
          }
        }
      }
    }
  `;

export const APPLY_COUPON_MUTATION = `
        mutation ApplyCoupon($cartId: String!, $couponCode: String!) {
          applyCouponToCart(input: { cart_id: $cartId, coupon_code: $couponCode }) {
            cart {
              id
              applied_coupons {
                code
              }
            }
          }
        }
      `;

export const REMOVE_COUPON_MUTATION = `
        mutation RemoveCoupon($cartId: String!) {
          removeCouponFromCart(input: { cart_id: $cartId}) {
            cart {
              id
              applied_coupons {
                code
              }
            }
          }
        }
      `;

2. Created a fetch handler in reactjs:

For reusability created a fetch handler function in file called FetchHandler.ts.

// src/component/cart/FetchHandler.ts

export interface GraphQLResponse<T> {
  data: T;
  errors?: Array<{ message: string }>;
}

export const fetchGraphQL = <T>(query: string, variables: Record<string, any> = {}): Promise<T> => {
  return fetch(YOUR_MAGENTO_ENDPOINT, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ query, variables }),
  })
    .then((response) => response.json())
    .then((result: GraphQLResponse<T>) => {
      if (result.errors) {
        throw new Error(result.errors.map((err) => err.message).join(", "));
      }
      return result.data;
    });
};

3. Some custom hooks to manage cart :

Here, I managed things by creating custom hooks according to their functionality in cart.ts

//custom-hooks to manage cart functionalities

import { useEffect, useState } from "react";
import { fetchGraphQL } from "./FetchHandler";
import {
  APPLY_COUPON_MUTATION,
  CREATE_EMPTY_CART_MUTATION,
  DELETE_CART_MUTATION,
  GET_CART,
  REMOVE_COUPON_MUTATION,
  UPDATE_CART_MUTATION,
} from "./queries";
interface CreateCartResponse {
  createEmptyCart: string;
}

export default function useCheckoutQuoteFetch() {
  const [cartId, setCartId] = useState<string | null>(() => {
    return localStorage.getItem("cartId") || null;
  });
  const savedItems = localStorage.getItem("cartItems");
  const [items, setItems] = useState<any>(() => {
    return savedItems ? JSON.parse(savedItems) : [];
  });

  const createCart = async () => {
    fetchGraphQL<CreateCartResponse>(CREATE_EMPTY_CART_MUTATION)
      .then((data) => {
        if (data.createEmptyCart) {
          localStorage.setItem("cartId", data.createEmptyCart);
          setCartId(data.createEmptyCart);
        }
      })
      .catch((error) => {
        console.error("Error creating cart:", error.message);
      });
  };
  useEffect(() => {
    if (!cartId) {
      createCart();
    }
  });

  const getCart = async () => {
    const variables = {
      cartId: cartId,
    };

    fetchGraphQL<{ cart: { id: string; items: Array<{ id: string; quantity: number }> } }>(GET_CART, variables)
      .then((response) => {
        const cart = response?.cart;
        console.log(cart);
        if (cart) {
          localStorage.setItem("cartItems", JSON.stringify(cart));
          setItems(cart);
        }
      })
      .catch((error) => {
        console.error("Error adding item to cart:", error.message);
      });
  };
  useEffect(() => {
    getCart();
  }, []);
  useEffect(() => {
    localStorage.setItem("cartItems", JSON.stringify(items));
  }, [items]);

  return { cartId, items, getCart };
}
export const useUpdateCartItem = () => {
  const [cartId, setCartId] = useState<string | null>(() => {
    return localStorage.getItem("cartId") || null;
  });

  const updateCartItem = async (itemId: string, quantity: string) => {
    const variables = {
      cartId,
      cartItems: [{ cart_item_uid: itemId, quantity: quantity }],
    };

    return fetchGraphQL<{ updateCartItem: { cart: any } }>(UPDATE_CART_MUTATION, variables)
      .then((data) => {
        return data.updateCartItem.cart;
      })
      .catch((err) => {
        console.error("Error updating cart item:", (err as Error).message);
      });
  };

  return { cartId, setCartId, updateCartItem };
};

export const useDeleteCartItem = () => {
  const [cartId, setCartId] = useState<string | null>(() => {
    return localStorage.getItem("cartId") || null;
  });
  const { getCart } = useCheckoutQuoteFetch();

  const deleteCartItem = (itemId: string) => {
    const variables = {
      cartId,
      itemId,
    };

    return fetchGraphQL<{ removeItemFromCart: { cart: any } }>(DELETE_CART_MUTATION, variables)
      .then((data) => {
        getCart();
        return data.removeItemFromCart.cart;
      })
      .catch((err) => {
        console.error("Error deleting cart item:", (err as Error).message);
      });
  };

  return { cartId, setCartId, deleteCartItem };
};

export const useApplyCoupon = () => {
  const [cartId, setCartId] = useState<string | null>(() => {
    return localStorage.getItem("cartId") || null;
  });
  const { getCart } = useCheckoutQuoteFetch();

  const applyCoupon = (couponCode: string) => {
    const variables = {
      cartId,
      couponCode,
    };

    return fetchGraphQL<{ applyCouponToCart: { cart: any } }>(APPLY_COUPON_MUTATION, variables)
      .then((data) => {
        getCart();
        return data.applyCouponToCart.cart;
      })
      .catch((err) => {
        console.error("Error applying coupon:", (err as Error).message);
      });
  };

  return { cartId, setCartId, applyCoupon };
};
export const useRemoveCoupon = () => {
  const [cartId, setCartId] = useState<string | null>(() => {
    return localStorage.getItem("cartId") || null;
  });
  const { getCart } = useCheckoutQuoteFetch();

  const removeCoupon = () => {

    const variables = {
      cartId,
    };

    return fetchGraphQL<{ removeCouponFromCart: { cart: any } }>(REMOVE_COUPON_MUTATION, variables)
      .then((data) => {
        getCart();
        return data.removeCouponFromCart.cart;
      })
      .catch((err) => {
        console.error("Error removing coupon:", (err as Error).message);
      });
  };

  return { cartId, setCartId, removeCoupon };
};
desktop view of cart page react.js

mobile view cart page reactjs
mobile view cart page reactjs

4. Cart Page UI Component :

In this component, I have successfully managed the shopping cart UI and its associated functionalities.

It integrates various features to provide a seamless and comprehensive shopping cart experience for users.

import React, { useEffect, useState } from "react";
import UpdateCartForm from "./CartUpdate";
import useCheckoutQuoteFetch, { useDeleteCartItem, useUpdateCartItem } from "./cart";
import ApplyCoupon from "./ApplyCoupon";
export const formatPrice = (value: number, currency: string) => {
  return value?.toLocaleString("en-US", {
    style: "currency",
    currency,
  });
};
export default function Cart() {
  useCheckoutQuoteFetch();
  const [items, setItems] = useState(() => {
    const cartQuote = localStorage.getItem("cartItems");
    return JSON.parse(cartQuote || "{}").items || [];
  });
  const [cartData, setCartData] = useState(() => {
    const cartQuote = localStorage.getItem("cartItems");
    return JSON.parse(cartQuote || "{}");
  });

  useEffect(() => {
    const handleStorageChange = () => {
      const cartQuote = localStorage.getItem("cartItems");
      const cartData = JSON.parse(cartQuote || "{}") || [];
      setItems(cartData.items || []);
      setCartData(cartData);
    };

    handleStorageChange();

    window.addEventListener("storage", handleStorageChange);

    return () => {
      window.removeEventListener("storage", handleStorageChange);
    };
  });
  const [itemData, setItemData] = useState({ qty: "", id: "" });
  const { updateCartItem } = useUpdateCartItem();
  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();
    updateCartItem(itemData.id, itemData.qty)
      .then((updatedCart) => {
        console.log("Updated cart:", updatedCart);
      })
      .catch((error) => {
        console.error("Failed to update cart item:", error);
      });
  };
  const { deleteCartItem } = useDeleteCartItem();

  const handleDelete = (itemId: string) => {
    deleteCartItem(itemId)
      .then((deleteCartItem) => {
        console.log("Updated cart after deletion:", deleteCartItem);
      })
      .catch((error) => {
        console.error("Failed to delete cart item:", error);
      });
  };
  const prices = cartData?.prices;
  const selectedMethod = cartData?.shipping_addresses?.[0]?.selected_shipping_method;
  return (
<div className="container mx-auto">
      <h1 className="px-4 mt-4 mb-10 text-5xl md:px-0 font-extralight">Shopping Cart</h1>
      <div className="grid gap-16 md:grid-cols-12">
        <div className="order-2 -mt-10 md:mt-0 md:col-span-8 md:order-1">
          <form onSubmit={handleSubmit} className="divide-y">
            <div className="hidden p-3 mt-6 text-sm font-medium md:flex columns-2">
              <p className="w-full">Item</p>
              <div className="flex items-center justify-between w-full">
                <p className="ml-9 md:ml-0">Price</p>
                <p className="ml-9 md:ml-0">Qty</p>
                <p className="ml-9 md:ml-0">Subtotal</p>
              </div>
            </div>
            {items.map((item: any) => (
              <div key={item?.cartItemId} className="px-4 py-6 md:px-2">
                <div className="md:columns-2">
                  <div className="flex gap-4">
                    <img
                      src={item?.product?.thumbnail?.url}
                      alt=""
                      className="object-cover object-center w-16 h-16 md:w-40 md:h-48"
                    />
                    <h2 className="text-lg font-light md:text-xl">{item?.product?.name}</h2>
                  </div>
                  <div className="flex items-center justify-between w-full gap-4 text-lg font-medium max-w-72 md:max-w-full md:text-xl">
                    <div className="flex flex-col gap-2">
                      <p className="text-sm font-medium md:hidden">Price</p>
                      <p>{formatPrice(item?.prices?.price?.value, item?.prices?.price?.currency)}</p>
                    </div>
                    <div className="flex flex-col gap-2">
                      <p className="text-sm font-medium md:hidden">Qty</p>
                      <UpdateCartForm itemId={item?.cartItemId} qty={item?.quantity} setItemData={setItemData} />
                    </div>{" "}
                    <div className="flex flex-col gap-2">
                      <p className="text-sm font-medium md:hidden">Subtotal</p>
                      <p>{formatPrice(item?.prices?.row_total?.value, item?.prices?.row_total?.currency)}</p>
                    </div>
                  </div>
                </div>{" "}
                <div className="flex items-center gap-4 ml-auto max-w-fit">
                  <svg
                    className="w-5 h-5 text-black"
                    fill="none"
                    strokeWidth={2}
                    stroke="currentColor"
                    viewBox="0 0 24 24"
                    xmlns="http://www.w3.org/2000/svg"
                    aria-hidden="true"
                  >
                    <path
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
                    />
                  </svg>
                  <button type="button" className="bg-transparent" onClick={() => handleDelete(item?.cartItemId)}>
                    <svg
                      className="w-5 h-5 text-black"
                      fill="none"
                      strokeWidth={3}
                      stroke="currentColor"
                      viewBox="0 0 24 24"
                      xmlns="http://www.w3.org/2000/svg"
                      aria-hidden="true"
                    >
                      <path
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
                      />
                    </svg>
                  </button>
                </div>
              </div>
            ))}
            <div className="flex justify-center py-6 md:justify-end md:my-6 border-y border-y-slate-300 md:border-y-0">
              <button type="submit" className="px-4 py-2 text-sm border border-solid border-neutral-300 bg-neutral-200">
                Update Shopping Cart
              </button>
            </div>
          </form>
          <div className="py-3 mb-10 md:py-0 border-y border-y-slate-300 md:border-y-0">
            <ApplyCoupon />
          </div>
        </div>
        <div className="order-1 w-full p-6 md:col-span-4 md:order-2 bg-neutral-100 max-h-fit">
          <div className="flex flex-col divide-y divide-slate-300">
            <p className="pb-4 text-2xl font-extralight">Summary</p>
            <div className="flex flex-col gap-4 py-4 text-sm font-light">
              <div className="flex items-center justify-between">
                <p>Subtotal</p>
                <p>{formatPrice(prices?.subtotal_excluding_tax?.value, prices?.subtotal_excluding_tax?.currency)}</p>
              </div>
              {prices?.discounts.length > 0 && (
                <div className="flex items-center justify-between">
                  <p>Discount({cartData?.applied_coupons?.[0]?.code || "Free"})</p>
                  {prices?.discounts?.map((item: { amount: { value: number; currency: string } }, index: number) => (
                    <p key={index}>{formatPrice(item?.amount?.value, item?.amount?.currency)}</p>
                  ))}
                </div>
              )}
              {selectedMethod && (
                <div className="flex items-center justify-between">
                  <p>Shipping({selectedMethod?.carrier_title})</p>
                  <p>{formatPrice(selectedMethod?.amount?.value, selectedMethod?.amount?.currency)}</p>
                </div>
              )}
            </div>
            <div className="flex items-center justify-between py-4 font-medium">
              <p>Order Total</p>
              <p>{formatPrice(prices?.grand_total?.value, prices?.grand_total?.currency)}</p>
            </div>
            <div className="flex flex-col items-center w-full">
              <button className="w-full px-8 py-4 my-6 font-medium text-white bg-sky-600 hover:bg-sky-700">
                Process To Checkout
              </button>
              <p className="text-sm font-normal text-sky-500">Check Out with Multiple Addresses</p>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

5. Update Cart Item

In this component, I have effectively managed the functionalities related to update item quantity.

import React, { useState } from "react";

interface UpdateCartFormProps {
  itemId: string;
  setItemData: any;
  qty: number;
}

const UpdateCartForm: React.FC<UpdateCartFormProps> = ({ itemId, setItemData, qty }) => {
  const [quantity, setQuantity] = useState<string>(`${qty}` || "1");
  const changeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuantity(e.target.value);
    setItemData({ qty: e.target.value, id: itemId });
  };
  return (
    <>
      <input
        className="w-16 p-2 text-sm font-normal text-center border"
        defaultValue={qty}
        type="text"
        value={quantity}
        onChange={(e) => changeHandler(e)}
        min="1"
        required
      />
    </>
  );
};

export default UpdateCartForm;

6. Coupon Handler:

In this component, I have effectively managed the functionalities related to applying and removing discounts.

This includes seamless integration of features that allow users to enter discount codes, view applied coupons, and easily toggle their application status, enhancing the overall shopping experience.

import React, { useState } from "react";
import { ChevronDownIcon } from "../Header";
import { useApplyCoupon, useRemoveCoupon } from "./cart";

export default function ApplyCoupon() {
  const [viewCoupun, setViewCoupon] = useState(false);
  const { applyCoupon } = useApplyCoupon();
  const cartQuote = localStorage.getItem("cartItems");
  const cartData = JSON.parse(cartQuote || "") || [];
  const [code, setCode] = useState(cartData?.applied_coupons?.[0]?.code || "");
  const handleSubmit = () => {
    applyCoupon(code)
      .then((coupon) => {
        console.log("Updated cart after applying coupon:", coupon);
      })
      .catch((error) => {
        console.error("Failed to apply coupon:", error);
      });
  };
  const { removeCoupon } = useRemoveCoupon();
  const handleRemove = () => {
    removeCoupon()
      .then((coupon) => {
        setCode("");
        console.log("Updated cart after removing coupon:", coupon);
      })
      .catch((error) => {
        console.error("Failed to remove coupon:", error);
      });
  };
  const appliedCoupon = cartData?.applied_coupons?.length > 0 ? true : false;
  return (
    <div className="px-4 md:px-0">
      <p className="flex items-center justify-between gap-4 text-sm md:max-w-fit md:text-base text-sky-500">
        Apply Discount Code{" "}
        <ChevronDownIcon
          onClick={() => setViewCoupon(!viewCoupun)}
          className={`w-4 h-4 ${viewCoupun ? "rotate-180" : "-rotate-0"}`}
        />
      </p>
      {viewCoupun && (
        <div className="flex items-center justify-between my-2 mb-6 border md:mb-0 border-slate-300 md:max-w-fit">
          <input
            type="text"
            className="px-3 max-w-44 focus:outline-0"
            value={code}
            onChange={(e) => setCode(e.target.value)}
          />
          <button
            type="button"
            className="px-3 py-1.5 text-sm bg-neutral-200"
            onClick={appliedCoupon ? handleRemove : handleSubmit}
          >
            {appliedCoupon ? "Cancel Coupon" : "Apply Coupon"}
          </button>
        </div>
      )}
    </div>
  );
}

7. Set Up Routes

Set your app.tsx in src according to your routing structure like this:

import React from "react";
import "./App.css";
import { Route, Routes } from "react-router-dom";
import Cart from "./component/Cart/Cart";
import Header from "./component/Header";

function App() {
  return (
    <div>
      <Header />
      <Routes>
        <Route path="/cart" Component={Cart} />
        {/* Other routes */}
      </Routes>
    </div>
  );
}

export default App;

Run Your ReactJs Application:

Finally, run your ReactJs app to see your cart page in action:

npm start

Conclusion

In this blog, we explored how to create a robust shopping cart page using React in conjunction with the Magento API.

We covered essential functionalities, including managing cart items, applying and removing coupons, and dynamically updating the cart based on user interactions.

Key takeaways include:

  • State Management: Leveraging React’s state hooks to manage cart data ensures a seamless user experience.
  • GraphQL Integration: Utilizing Magento 2 GraphQL API for fetching and updating cart information allows for efficient data handling and flexibility in managing cart operations.
  • User Interactions: Implementing custom hooks and components enhances the functionality of the shopping cart, allowing for features like item updates and coupon management.
  • Local Storage: Managing cart data in local storage enables persistence, ensuring users don’t lose their cart items upon page refresh.

By following these principles and utilizing the components, developers can create an engaging and efficient shopping cart experience.

That integrates smoothly with Magento 2 powerful e-commerce capabilities.

With this foundation, you can further expand features, improve performance, and refine the user interface to meet specific business needs.

. . .

Leave a Comment

Your email address will not be published. Required fields are marked*


Be the first to comment.

Back to Top

Message Sent!

If you have more details or questions, you can reply to the received confirmation email.

Back to Home