TL;DR: Want to design a user-friendly e-commerce app for digital products with React? This guide unlocks the secrets to building a beautiful and functional Home Page, including showcasing products and adding to cart features. Dive in to learn more!
Welcome to the fourth blog of the series, “Designing a React E-Commerce App for Digital Products”. In the first part, we created an architectural diagram to create the user navigation and wireframes. We also set up the boilerplate, complete with public and private routes.
In the second part, we implemented the web app’s authentication and authorization. This involved the creation of signup and login pages and their integration with a database for data persistence and session maintenance.
In the third part of the series, we created a dashboard where users can manage their products, from adding and updating to previewing them before publication.
Now, in part 4, we’re creating the home page. This component will showcase all available products, allowing users to browse, add items to their cart, and proceed to checkout. Our goal is to ensure that all products, whether created by the users or officially published, are prominently displayed on the home page. Additionally, we’ll develop separate components for the cart and checkout pages. The cart page will allow users to review selected items, while the checkout page will facilitate secure purchases.
To enhance accessibility, we’ll update our routing configuration. The product listing page and cart page will be public routes, accessible to all users. However, the checkout page will be a private route, requiring user authentication.
Refer to the following code example to set up the routing configuration for the application, directing users to different pages based on their authentication status in the App.js file.
import { BrowserRouter, Routes, Route } from "react-router-dom"; import Login from "./pages/login"; import Signup from "./pages/signup"; import PrivateRoutes from "./routes/private"; import PublicRoutes from "./routes/public"; import Dashboard from "./pages/dashboard"; import ManageProduct from "./pages/manageProduct"; import NotFound from "./pages/404"; import ProductPreview from "./pages/productPreview"; import Browse from "./pages/browse"; import Cart from "./pages/cart"; import Checkout from "./pages/checkout"; import ThankYou from "./pages/thankYou"; const App = () => { return ( <BrowserRouter> <Routes> <Route path="/" element={<PublicRoutes />}> <Route index element={<Browse />} /> <Route path="product-list" element={<h1>Product List</h1>}></Route> <Route path="login" element={<Login />}></Route> <Route path="signup" element={<Signup />}></Route> </Route> <Route path="/" element={<PrivateRoutes />}> <Route path="dashboard" element={<Dashboard />}></Route> <Route path="manage-products" element={<ManageProduct />}></Route> <Route path="checkout" element={<Checkout />}></Route> <Route path="thank-you" element={<ThankYou />}></Route> </Route> <Route path="/product/:id" element={<ProductPreview />} /> <Route path="/cart" element={<Cart />} /> <Route path="*" element={<NotFound />} /> </Routes> </BrowserRouter> ); }; export default App;
When designing the home page in the browse.js file, we will utilize the DataGrid component again to display a list of products available for browsing.
browse.js
import "./productList.css"; import { Link } from "react-router-dom"; import useProducts from "../hooks/useProducts"; import productStore from "../store/products"; import cartStore from "../store/cart"; import DataGrid from "../components/DataGrid"; import Wrapper from "./browseWrapper"; const Browse = () => { useProducts(); const productsList = productStore((state) => state.productsList); const setCartItems = cartStore((state) => state.setCartItems); const addProductToCart = (product) => { setCartItems({ [product.id]: product }); }; const ActionsTemplate = (e) => { return ( <span> <span onClick={() => addProductToCart(e)} className="edit-product"> Add to cart </span> / <Link to={`/product/${e.id}`} title={`preview ${e.Name}`}> Preview </Link> </span> ); }; return ( <Wrapper> <div className="product-list"> <DataGrid data={productsList} ActionsTemplate={ActionsTemplate} /> </div> </Wrapper> ); }; export default Browse;
The product list fetches all available products, so we’ll create a separate hook for this purpose named useProducts.js. Upon component mounting, this hook will store the fetched data in a Zustand store to cache it on the client.
Refer to the following code example.
useProducts.js
import { useState, useEffect } from "react"; import useSupbase from "./supbase"; import productStore from "../store/products"; const useProducts = () => { const setProductsList = productStore((state) => state.setProductsList); const supabase = useSupbase(); const fetchProducts = async () => { const { data: productData, error: productError } = await supabase .from("products") .select(); setProductsList(productData); try { } catch (e) { console.error("Error while fetching products list"); } }; useEffect(() => { fetchProducts(); }, []); }; export default useProducts;
The product items will have an Add to cart button to facilitate adding items to the cart. This cart is managed on the client side in the Zustand and stores the added items for each specific user.
store/cart.js
import { create } from "zustand"; import { persist } from "zustand/middleware"; let store = (set) => ({ items: {}, setCartItems: (item) => set((state) => ({ items: { ...state.items, ...item } })), updateCartItems: (items) => set(() => ({ items })), resetCart: () => set(() => ({ items: {} })), }); //persist the state with key "randomKey" store = persist(store, { name: "user-cart" }); //create the store let useStore = create(store); export default useStore;
These stored cart items will be displayed on the cart page, while the number of items in the cart will be visible in the header. Therefore, we need to create a wrapper around the home page and cart page with a header that will show the number of items in the cart.
browserWrapper.js
import "@syncfusion/ej2-layouts/styles/material.css"; import "@syncfusion/ej2-react-buttons/styles/material.css"; import "@syncfusion/ej2-base/styles/material.css"; import "@syncfusion/ej2-react-navigations/styles/material.css"; import "./wrapper.css"; import { Link, NavLink } from "react-router-dom"; import useStore from "../store/user"; import cartStore from "../store/cart"; const Wrapper = ({ children }) => { const { isLoggedIn, firstName } = useStore((state) => state); const items = cartStore((state) => state.items); return ( <div className="wrapper"> <header> <Link className="title" to="/" title="visit dashboard"> Geva Digital Shop </Link> <div> {isLoggedIn ? ( <span className="username">Hello {firstName}</span> ) : ( <NavLink to="/login" className={({ isActive }) => (isActive ? "active" : "")} > Login </NavLink> )} / <NavLink to="/cart" className={({ isActive }) => (isActive ? "active" : "")} > Cart ({Object.keys(items).length}) </NavLink> </div> </header> <main>{children}</main> </div> ); }; export default Wrapper;
Clicking the cart button in the header will take us to the cart page.
The cart page utilizes the DataGrid component to display products added to the cart by accessing them from the cart store (Zustand).
cart.jsx
import "./productList.css"; import cartStore from "../store/cart"; import useStore from "../store/user"; import Wrapper from "./browseWrapper"; import DataGrid from "../components/DataGrid"; import { Link } from "react-router-dom"; import "./cart.css"; const Cart = () => { const isLoggedIn = useStore((state) => state.isLoggedIn); const { items, updateCartItems } = cartStore((state) => state); const removeCartItems = (product_id) => { const itemsClone = { ...items }; delete itemsClone[product_id]; updateCartItems(itemsClone); }; const ActionsTemplate = (e) => { return ( <span> <span onClick={() => removeCartItems(e.id)} className="edit-product"> Remove </span> </span> ); }; const cartItems = Object.keys(items).map((e) => items[e]); return ( <Wrapper> <div className="cart-items"> <DataGrid data={cartItems} ActionsTemplate={ActionsTemplate} /> <div class="purchase-area"> {isLoggedIn ? ( <Link to={items.length === 0 ? "" : "/checkout"}>Continue to purchase</Link> ) : ( <Link to="/login">Login to purchase</Link> )} </div> </div> </Wrapper> ); }; export default Cart;
cart.css
.purchase-area { text-align: center; padding: 20px; margin: 20px; } .purchase-area a { text-align: center; padding: 20px; display: inline-block; text-decoration: none; font-size: 1.2em; color: #fff; background: #4caf50; border-bottom: 1px solid #f7f7f7; transition: all 0.2s ease; } .purchase-area a:hover { color: #000; }
On the cart page, we check if the user is logged in or not. If they are not logged in, we prompt them to log in by displaying a Login to purchase button. On clicking this button, they will be redirected to the login page.
After logging in, they will be redirected to the dashboard page, where they can still see the cart in the header. Clicking on the cart in the header will navigate them to the checkout page.
The checkout page is one of the most crucial parts of any e-commerce site, as it is where users are most likely to abandon their carts if there are too many fields to fill out.
Thus, we aim to keep it simple by using two layout designs. On the right side, we show the cart items and the total cart value. On the left side, we display a form for users to fill out to complete the purchase.
checkout.jsx
const getCartTotal = (items) => { const itemsMapped = Object.keys(items).map((e) => items[e]); const cartTotal = itemsMapped.reduce((a, b) => { const { product_price } = b; return a + Number(product_price); }, 0); return cartTotal; }; const CartDetails = () => { const items = cartStore((state) => state.items); const itemsMapped = Object.keys(items).map((e) => items[e]); const cartTotal = getCartTotal(items); const productsMapped = itemsMapped.map((e) => <Products item={e} key={e} />); return ( <div className="product-wrapper"> <div>{productsMapped}</div> <div> Cart Total: <strong>{cartTotal}</strong> </div> </div> ); }; const Products = ({ item }) => { return ( <div className="product" key={item.product_name}> <div> <img src={item.product_thumbnail} alt={item.product_name} /> </div> <br /> <div>{item.product_name}</div> <br /> <div> Price: <strong>{item.product_price}</strong> </div> </div> ); };
Syncfusion offers a set of input fields that are ready to use. Follow this guide on creating a form with validation.
We are creating a shipping form to gather the user’s shipping details. This form includes fields for the user’s:
Each field undergoes validation and is marked as required. If a user tries to submit the form without filling in these fields, validation errors will appear.
checkout.jsx
const ShippingForm = () => { const navigate = useNavigate(); const { items, resetCart } = cartStore((state) => state); const supabase = useSupbase(); const userNameRef = useRef(null); const formObject = useRef(null); const initialState = { name: "", state: "", country: "", address: "", postal_code: "", phone_number: "", }; const reducer = (state, action) => { switch (action.type) { case "update": return { ...state, [action.field]: action.value }; default: return initialState; } }; const [state, dispatch] = useReducer(reducer, initialState); const update = (field) => (event) => { //update action is dispatched to update the email and password state value. dispatch({ type: "update", field, value: event.value }); }; // checks the length of mask value and returns corresponding Boolean value. const customFn = (args) => { const argsLength = args.element.ej2_instances[0].value.length; return argsLength >= 10; }; useEffect(() => { userNameRef.current.focusIn(); const options = { // validation rules rules: { name: { required: [true, "* Please enter your full name"], }, phone_number: { numberValue: [customFn, "* Please enter your phone number"], }, address: { required: [true, "* Please enter your address"], }, postal_code: { required: [true, "* Please enter your postal code"], }, state: { required: [true, "* Please enter your state"], }, country: { required: [true, "* Please enter your country"], }, }, }; // Initialize the form validator formObject.current = new FormValidator("#form1", options); }, []); const onSubmit = async (e) => { e.preventDefault(); try { const { data, error: userError } = await supabase.auth.getSession(); const { session } = data; const address = JSON.stringify(state); const products = JSON.stringify(items); const total = getCartTotal(items); const { data: orderData, error: orderError } = await supabase .from("order") .insert({ total, address, products, user_id: session?.user?.id, }) .select(); if (!orderError) { formObject.current.element.reset(); resetCart(); navigate("/thank-you", { state: { orderId: orderData[0].id } }); } } catch (e) { console.error("Something went wrong", e); } }; return ( <> <div id="container"> <div> <div className="control_wrapper" id="control_wrapper"> <h3 className="form-title">Fill in your shipping details</h3> <div className="control_wrapper textbox-form"> <form id="form1" method="post"> <div className="form-group"> <TextBoxComponent ref={userNameRef} name="name" value={state.email} change={update("name")} placeholder="Full Name" floatLabelType="Auto" data-msg-containerid="errorForName" /> <div id="errorForName" /> </div> <div className="form-group"> <MaskedTextBoxComponent mask="000-000-0000" id="mask" name="phone_number" placeholder="Phone Number" floatLabelType="Always" data-msg-containerid="errorForPhone" value={state.phone_number} change={update("phone_number")} /> <label className="e-error" htmlFor="phone_number" /> </div> <div className="form-group"> <div className="e-float-input"> <textarea className="address-field" id="address" name="address" value={state.address} onChange={update("address")} /> <label className="e-float-text e-label-top">Address</label> </div> </div> <div className="form-group"> <TextBoxComponent type="text" name="postal_code" value={state.postal_code} change={update("postal_code")} placeholder="Postal code" floatLabelType="Auto" data-msg-containerid="errorForPostalCode" /> <div id="errorForPostalCode" /> </div> <div className="form-group"> <TextBoxComponent type="text" name="state" value={state.state} change={update("state")} placeholder="State" floatLabelType="Auto" data-msg-containerid="errorForState" /> <div id="errorForState" /> </div> <div className="form-group"> <TextBoxComponent type="text" name="country" value={state.country} change={update("country")} placeholder="Country" floatLabelType="Auto" data-msg-containerid="errorForCountry" /> <div id="errorForCountry" /> </div> </form> <div className="submitBtn"> <ButtonComponent cssClass="e-success e-block" onClick={onSubmit} > Submit </ButtonComponent> </div> </div> </div> </div> </div> </> ); };
Putting both together in the Checkout component allows for a cohesive display of the shipping form and cart details.
const Checkout = () => { return ( <div className="checkout-wrapper"> <main className="e-card"> <ShippingForm /> </main> <aside id="default-sidebar" className="e-card sidebar" position="Right"> <CartDetails /> </aside> </div> ); }; export default Checkout;
checkout.css
.checkout-wrapper { max-width: 800px; margin: 100px auto 0 auto; display: flex; } .checkout-wrapper > main { padding: 40px; } .checkout-wrapper > aside { width: 300px; margin-left: 15px; padding: 5px; } .checkout-wrapper .form-group { margin: 10px; } .checkout-wrapper .product-wrapper { text-align: center; } .checkout-wrapper img { width: 100px; } .checkout-wrapper .product { font-size: 14px; line-height: 1.3em; margin: 20px 0; border-bottom: 1px solid; }
Clicking the SUBMIT button adds an entry into the database and clears the local cart store. We’ve created a new table order in Supabase for this purpose.
create table public.order ( id bigint generated by default as identity, created_at timestamp with time zone not null default now(), total integer null, products json null, address json null, user_id character varying null, constraint order_pkey primary key (id) ) tablespace pg_default;
Then, insert the record into it by clicking the button.
const onSubmit = async (e) => { e.preventDefault(); try { const { data, error: userError } = await supabase.auth.getSession(); const { session } = data; const address = JSON.stringify(state); const products = JSON.stringify(items); const total = getCartTotal(items); const { data: orderData, error: orderError } = await supabase .from("order") .insert({ total, address, products, user_id: session?.user?.id, }) .select(); if (!orderError) { formObject.current.element.reset(); resetCart(); navigate("/thank-you", { state: { orderId: orderData[0].id } }); } } catch (e) { console.error("Something went wrong", e); } };
After successfully adding the record, we navigate to the Thank you page with the order ID.
On the thank you page, we display the order ID of the purchase. If a user tries to access the thank you page directly without making a purchase, they are redirected to the dashboard page.
import { useLocation, useNavigate } from "react-router-dom"; const ThankYou = () => { const navigate = useNavigate(); const { state } = useLocation(); if (!state) { navigate("/dashboard"); return; } const { orderId } = state; return ( <div className="checkout-wrapper"> <div className="e-card text-center"> <h1>Thank you for your purchase</h1> <span>Your order id: {orderId}</span> </div> </div> ); }; export default ThankYou;
Refer to the following image.
We appreciate your time in reading this blog! In it, we’ve detailed the design and implementation of the home page, cart management, checkout process, and thank you page, all aimed at giving users a seamless shopping experience in our React digital products e-commerce app. We urge you to apply what you’ve learned and share your journey in the comments below!
Syncfusion’sEssential Studio® for React is a one-stop solution offering a wide range of high-performance, lightweight, modular, and responsive UI components. It’s the perfect suite to build any web app.
For our existing Syncfusion users, the latest version of Essential Studio® is available for download on the License and Downloads page. If you’re new to Syncfusion, we invite you to explore our products’ features and capabilities with our 30-day free trial.
For questions, you can contact us through our support forum, support portal, or feedback portal. We are always here to assist you!
See you in our next blog!