How To Make A Really Good Shopping Cart With React

by kleamerkuri
Online coffee order app React shopping cart.

What if your React skills could brew up more than just UI magic? Ready yourself for a coding adventure where we’ll harness the power of React to craft an extraordinary shopping cart experience.

Today, we’re delving deep into state management, React components, and the art of seamless user interaction.

We’ve been exploring React for a while now. Check out our last main project, Inspire App, which puts a spin on a freeCodeCamp challenge.

While building Inspire, we used hooks like useEffect and useContext to create and deploy a React app with secrets.

In this project, we’ll advance our skills with the application of useReducer to manage the complex state of a shopping cart.

If you haven’t built a shopping cart before, don’t worry. Neither have I 😅

Ready to code beyond the ordinary?

Hey! Check out the live version of the coffee order app. It’s the end result we’ll achieve in this post.

Starting out

Let’s begin by creating a plan (i.e. what assets, styles, etc. we’ll use) and by initializing a React project.

Logo ideation

To kick things off, we need to brainstorm a standout logo. Since my theme is a chic coffee order app called “coin caffé”, I opted to logo-fy the name.

Coin Caffé is French for “corner coffee” and I think it’s self-explanatory as to the why. Ironically, here I’m justifying it because DM maintains “It’s only self-explanatory in your head” 🙄

Well, who doesn’t want a coffee shop just around the bend of their house? Where they can take a short walk to grab a cup and bask in the glory of glorious aromas?

Even talking about coffee makes me excited so I’ll keep myself in check by moving on to the next step: Project Design.

Gathering design inspiration

Since we have a solid theme—a coffee order app—and a brand name, we’re ready to bring the rest of the stylistic elements necessary to complete the design of the project.

I always try to brainstorm a project plan before coding anything. Experience taught me that if you skip the planning, you’ll come to regret the wasted time fumbling with code.

So, let’s embark on a visionary field trip to your local coffee shop.

Absorb the ambiance, and note the colors, textures, and vibes.

Capture the essence of that perfect coffee moment—cozy, inviting, and filled with anticipation. Your app should reflect this experience.

Since I imagine a chic, modern-ish coffee experience, I settled on a neutral color landscape with clean strokes.

Online coffee order app menu list.

I did some research for the design of the shopping cart and menu list, eventually settling on the display of data like:

  • coffee image
  • coffee name and description
  • item price and quantity
  • option to add/remove an item to/from the cart
Coin cafe ideation.

Note 🧐
Most of the data to be displayed relies on the source. You’ll see, as we continue, that I’ll customize some of the source data to suit project needs.

Setting up a local dev environment

Pull up your code editor, do the terminal tango, and set up your React app shell.

Hey! 👀
Do you need to glam your code editor? If you’re a VS Coder like me, THT has now two custom themes available to install for free! Get Pink Panda for a cozy, dark experience but grab Rhysand for an ethereal twist 🔥

If you need some help, check out How To Start Your React Journey: Create React App (Part 1) for how to get started with a React project.

Don’t worry if you’re new to React—we have a beginner project that walks you step-by-step through a full React project. It was my first one…it’s beginner-friendly to a “B” 😌

Now run your local development server, and let’s dive into the world of code and caffeine.

Adding components

Remember our initial project design and planning? It was with a purpose that goes beyond the aesthetic.

We’ll use the project design concept to break down our coffee order app into React components.

My src directory looks something like this:

- src
	-- App.js
	-- index.js
	.
	.
	.
	- Components
		- Nav
			-- Nav.js
			-- Carts.js
		- CartCount.js
		- CartList.js
		- Header.js
		- Menus.js
		- MenuItem.js
		- Modal.js
		- ScrollToTop.js

Note: This is a simplified overview. As you’ll see, the actual src directory structure is more involved as we add styles, a context, and other utils.

Header component

Start with a sleek Header component that comprises:

  • top navigation
  • page marquee
// Header.js
import Nav from './Nav/Nav';

import s from './Header.module.scss';

const Header = () => {
    return(
        <div className={s.container}>
            <Nav />
            <div className={s.content}>
                <h1>Welcome to Coin Cafe, where every cup is a treasured moment.</h1>
                <p>Step inside and experience the currency of great taste, where we brew not just coffee but connections. Join us for a journey through flavors that enrich your senses, and savor the moments that make life priceless.</p>
            </div>
        </div>
    )
}

export default Header;

I break down the navigation into a Nav component that includes the logo and page action (i.e. shopping cart and menu) as the Cart component.

// Nav.js
import Cart from './Cart';

import s from './Nav.module.scss';

const Nav = () => {
    return(
        <div className={s.container}>
            <div className={s.nav_items}>
                <div className={s.logo}>
                    <span>coin café</span>
                </div>
                <Cart />
            </div>
        </div>
    )
}

export default Nav;

Meanwhile, the page marquee container holds the marquee background image at full width.

I chose to reference the image by its URL as a shortcut instead of downloading and hosting it. The downside of doing this is that you cannot optimize the asset.

Since this is a low-impact project, there’s no fear of affecting performance so we’re good. But, if you want to go the extra leg, an alternative is to add the optimized image in the src folder inside of an assets directory.

Cart button component

I have two requirements for the shopping cart button:

  1. Display the count of items in the cart, if there are any
  2. Show full cart on click
// Cart.js
import { useContext } from 'react';

import CartContext from '../../Context/cart-context';

import s from './Cart.module.scss';

export default function Cart() {
    let { items, showCart } = useContext(CartContext);

    let totalItemCount = items.reduce((currNum, item)=>{
        return currNum + item.count;
    }, 0);

    const showCartHandler = () => {
        showCart(true);
    }
    

    return ( 
        <div className={s.container}>
            <div className={s.cart_icon} onClick={showCartHandler}>
                <button type='button'>
                    <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAACrUlEQVR4nO2ZO4xMURjHf1YyjalorAqVkXg0VuFViA2xGla2oGIlXhGroluFR8Go6GzhHatCRWM7q/Co2MSuhFgVjV2yEhn55H/lkp2559wZe7+IX3KSyZzv/93/yb333O+cA/+pSwnoAW4Cr4AJNft9Q30W45qdwBhQy2ijwA4cMhuopow+BQ4DFWCOWkX/PUvFnZPWDVUZ+wrsAWY1iG0DehWbDMbN41QDvgBrInRrU4PZTsGUUu+E3YlY9kn7uugJoCf1TtgjE4tpniuH3dnCuCUTh5rIcUQ5rlMgIzKxpIkcS5XDvjOF8Vkmyk3kKCuH5SqM5HvgJc+/P5DJgHJjptpk3kHMc2C+9kebm2cgKwNmk5l6tEbUvyJP8m0SP2jCQChZeR6qvytP8oMSD9Tp35gyYL/zEpJnQP3mKZqzEvfX6R9NGbBaKS8heU6q/0yeC1yT2Eru6XiXMvA2zwUi8vSq3zxFMyRxZ53+LTJhF9+c5wIReTrlxTxF86YFdVSrqMiLLRmiy+upFtRRraIsL99ilwsLJPyIHz7JU3uMaLVEtujxwgt56sizDr+LH+7JU3eM6JhEF/HDJXnqixFdkOg4fjghT7b1FMwdiXbhh93yNBgjeiLROvywXp6GY0QfJFqIHxbJ03iowDbKvqt52jUvxfparJG/xx/j8mZ3J5MNCn6MP4blzd6X4NnhNv4YjJlNk/n6PP6oxnzfki/oUfzRF1Nx3Fewx6Oxbnmzuiu4ylyFPzpiqvKk7p+PP9pD10nJSmwq58HN36YtdOWarI1ti8YrYyF7CcluxSP8MiSPmxoF7VXQFfxyNeTwtV9Bp/DL6Ywd0N/2WPfjlwPyeJmAXe+t+KUr4JTg51mIBS3DL8vl8WWjoAkHp1K1wGZep8XjUVtW+3UU9wNY5IVjWCP9OAAAAABJRU5ErkJggg==" alt='' />
                </button>
                {
                   totalItemCount > 0 && <span className={s.item_count}>{totalItemCount}</span> 
                }
            </div>
            <div className={s.menu_icon}>
                <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAARklEQVR4nO3WsQkAMQwEweu/LKmwd2wwOLZ+BlTAouQSAGDTSb7HrqeE1CkEAACuXly/PSWk7v8BAIAh67enhNQpBAB+awHE9RApipkrkgAAAABJRU5ErkJggg==" alt='' />
            </div>
        </div>
    )
}

Notice how I’m referencing a context. We don’t have that context yet; we’ll be creating it next.

But I know my context must have an items property that holds the cart items and a showCart method that acts as a flag to control the display of the cart.

Header component with shopping cart navigation.

Adding cart context

I’ll elevate the app’s architecture with a cart-context.

// cart-context.js
import React from "react";

const CartContext = React.createContext({
  items: [],
  totalAmount: 0,
  isCartShowing: false,
  addItem: (item) => { },
  removeItem: (id) => { }, 
  showCart: () => { }
});

export default CartContext;

The context consists of three properties and three methods:

  • items: An array of cart items
  • totalAmount: The total dollar amount of cart items
  • isCartShowing: A boolean flag that identifies the display of the cart
  • addItem & removeItem: Methods that take an identifier for the item to add/remove and update the items list
  • showCart: Determines whether the cart should be showing or not

Every component can access cart data effortlessly, making the codebase as interconnected as the coffee community itself 🥲

Menu component

Here comes the heart of the app—the Menu component. This component has the function of fetching coffee data. The display of data depends on the sub-component MenuItem.

// Menu.js
import { useEffect, useState } from 'react';

import MenuItem from './MenuItem';
import ScrollToTop from './ScrollToTop';

import s from './Menu.module.scss';

let URL = '<https://thehelpfultipper.github.io/coffee_data_custom/coffee.json>';

const Menu = () => {
    let [data, setData] = useState([]);

    const getCoffeeData = async () => {
        try {
            const resp = await fetch(URL);
            const data = await resp.json();
           
            setData(data);
        } catch (e) {
            console.log(e)
        }
    }

    useEffect(() => {
        getCoffeeData(); // desc, id, img, title 
    }, []);

    return (
        <div className={s.container}>
            {data.map((item, i) => {
                return <MenuItem data={item} id={i} key={i} />
            })}
            <ScrollToTop />
        </div>
    )
}

export default Menu;

Customizing fetched data

Initially, I fetched and processed data on the client side, but I ran into too many error instances due to the Coffee API’s server failing at random times.

Very unreliable 😒

To avoid a sucky user experience, I resorted to setting up a Node.js application that fetches data from a URL and writes it to a JSON file.

The construct of the app is as follows:

  • coffeeData.js: Holds action that fetches and manipulates data
  • app.js: Sets the parameters for the data fetching action and executes the retrieval
  • coffee.json: Result with coffee data in a format ready to consume by React application

I only need to run the fetch once (or if I want to update the data) so going the JSON route makes sense.

💡 See the code on the custom coffee data repo.

Since Coffee API has two endpoints—one for hot, and the other for cold—I’ll fetch data from both API endpoints and combine it into a single data array.

// coffeeData.js

// @urls is array of URLs to fetch data 
export async function getCoffeeData(urls, filePath) {
    try {
        // Fetch coffee data
        let allData = [];

        const responses = await Promise.all(urls.map(url => axios.get(url)));
        const data = await Promise.all(responses.map(resp => resp.data));

        data.forEach(type => allData.push(...type));

        // Do something with data ~ add price & quantity
        let customData = allData.map( item => ({...item, price: generateRandomPrice(), count: 1}));

        // Write data to JSON file
        await writeFile(filePath, JSON.stringify(customData, null, 2));
        console.log('Data written to ', filePath);
        return 'Data written successfully';
    } catch (err) {
        console.log('Error fetching data or writing to file: ', err);
        throw 'Error fetching data or writing to file.';
    }

}

The first Promise.all() produces an array of response promises that require another Promise.all() to get resolved.

I’m adding two custom properties to each coffee data object: price and count because the API data doesn’t have them.

Modified menu item data on fetch.

Once we generate coffee.json, I host the JSON file using GitHub pages and fetch its data in the Menu component.

Displaying each menu item

Send the fetched data to the MenuItem component that is responsible for displaying the info for each coffee.

Here’s where I spruce things for a show like polishing the description part by cutting down on the words.

// MenuItem.js
const getFirstSentence = (text) => {
    const sentences = text.match(/[^.!?]+[.!?]+/);
    
    return sentences ? sentences[0].trim() : text.trim();
}

Use a regular expression to split the text into sentences then make a logical check to return the original sentence (if there’s only one).

The regex looks for sequences of characters that end with a period, exclamation point, or question mark, which are common sentence-ending punctuation marks.

I go a step further by adding a scroll-to-top option. Scroll-to-top is a little extra function that’s easy to implement and super user-friendly!

// ScrollToTop.js
import s from './ScrollToTop.module.scss';

const ScrollToTop = () => {
    const handleScroll = () => {
        window.scrollTo({
            top: 0,
            behavior: 'smooth'
        });
    }

    return (
        <div className={s.scroll_wrapper}>
            <button 
                onClick={handleScroll} 
                type='button'
                className={s.scroll_btn}
            >^</button>
            <span>Scroll to top</span>
        </div>
    )
}

export default ScrollToTop;

The next, step is to connect the increment count for each menu item with the cart.

CartCount component

The CartCount component provides a seamless shopping experience by rendering the add/remove button controls.

Users can add or remove items with a simple click.

// CartCount.js
import s from './CartCount.module.scss';

const CartCount = ({count, onAddToCart, onRemoveFromCart}) => {
    return (
        <div>
            <button 
                type='button' 
                className={`${s.cartBtn} ${s.rem}`} 
                onClick={onRemoveFromCart}
            >−</button>
            <span className={s.count}>{count}</span>
            <button 
                type='button' 
                className={`${s.cartBtn} ${s.add}`} 
                onClick={onAddToCart}
            >+</button>
        </div>
    )
}

export default CartCount;

This component passes pointers to functions that execute upon certain events firing. It really isn’t majestic.

It’s all in the details, however. Paying attention to small utilitarian things like this improves user experience, taking an app from good to great 💁‍♀️

Hoping onto MenuItem, see how the CartCount pointers execute the updating context functions:

// MenuItem.js
const MenuItem = ({data, id}) => {
    let [ count, setCount ] = useState(0); 
    let {items, addItem, removeItem} = useContext(CartContext);

    const addToCartHandler = (item=data) => {
        addItem(item);
        // Update item count in menu 
        setCount(prevCount => prevCount + 1);
    }
;
    const removeCartHandler = (id) => {
        removeItem(id);
        // Update item count in menu 
        setCount(prevCount => {
            if(prevCount === 0 || items.count === 0) return 0;
            return prevCount - 1;
        });
    }

    return (
        <div className={s.wrapper} id={id}>
            <div className={s.img}>
                <img src={data.image} alt={data.title} />
            </div>
            <div className={s.desc}>
                <h3>{data.title}</h3>
                <p>{getFirstSentence(data.description)}</p>
            </div>
            <div className={s.price}>
                <span>${data.price}</span>
                <CartCount 
                    count={count}
                    onAddToCart={addToCartHandler.bind(null, data)} 
                    onRemoveFromCart={removeCartHandler.bind(null, data.id)} 
                />
            </div>
        </div>
    )
}

export default MenuItem;

Notice that I use bind to preconfigure the arguments passed onto the CartItem component.

This pattern is often used when you want to pass functions along with specific data to child components.

Binding ensures that the function is called with the correct data, even if the function and data come from different parts of the application.

The first argument is the context to which the function will be bound. In our case, it’s null, meaning that the function isn’t bound to any specific context.

Meanwhile, the second argument is the data that gets executed with the function.

Tip ✨
Don’t get confused with passing data from child to parent. In such a case, the function is initialized with an expecting argument and only a pointer is passed to the child component. In the child, the function gets executed with data originating from the child. That’s not this case 🙅‍♀️ Here, the data exists in the parent; we want to execute the function along with the data in the child.

Read: How To Master React State & Event Handlers (Part 4)

Adding a modal with React portal

Let’s now create a modal for the shopping cart. This will display only once a user clicks on the cart icon in the nav.

The modal consists of the backdrop and content overlay, both ported to the overlays root element using React Portals.

This way we gracefully overlay the modal, giving users a clear view of their chosen delights without losing context.

// Modal.js
import { Fragment, useContext } from 'react';
import { createPortal } from 'react-dom';

import CartContext from '../Context/cart-context';

import s from './Modal.module.css';

const Backdrop = () => {
    let { showCart } = useContext(CartContext);

    const showCartHandler = e => {
        if(e.target.classList[0].includes('backdrop')) {
            showCart(false);
        } 
    }

    return (
        <div className={s.backdrop}  onClick={showCartHandler}></div>
    )
};

const OverlayContent = (props) => {
    return (
        <div className={s.overlay}>
            {props.children}
        </div>
    )
}

const Modal = (props) => {
    return(
        <Fragment>
            {createPortal(<Backdrop />, document.querySelector('#overlays-root'))}
            {createPortal((<OverlayContent>{props.children}</OverlayContent>), document.querySelector('#overlays-root'))}
        </Fragment>
    )
}

export default Modal;

I went a step further in making the Modal component reusable by creating a modal wrapper component using props.children.

Managing cart and modal state

Time to take control of our state which is quite complex in case you haven’t figured it out yet.

Recall those methods in the cart-context. They’re updating functions that will update the state of existing variables.

If you’ve been wondering how the heck I’m using the context when I haven’t designated a provider, you’re about to find out.

Since we’re dealing with complex states, I’ll be using useReducer to manage the various cart-related states.

A look at useReducer

Use useReducer when updating a state that depends on another state (s) like that of form validity based on the state of the inputs.

useReducer returns an array with two values:

  1. a state snapshot (of the current state)
  2. a dispatch function (to update the current state)

An example: const [state, dispatchFunc] = useReducer(reducerFunc, initState, initFunc)

Breaking it down:

initFunc

Used in case the initial state is more complex

reducerFunc

Takes two parameters—state and action—and returns a new state.

The reducer function can be defined beyond the scope of the component function because it doesn’t need to interact with anything inside.

dispatchFunc

Provides instructions for the state update, usually looking like this:
dispatchFunc({type: "STR", val: <payload>})

The dispatch function takes an action that’s often an object with:

  • an identifier called “type” whose value is a string (conventionally in all-caps) that describes what happened
  • an optional payload

Dispatching an action triggers the automatic firing of the reducer function. You can also provide an optional payload to the dispatch function, something we’ll do below.

Moving on, the reducer function receives the latest state and returns a new state similar to useState with syntax: (prevState, action) => newState.

Defining the cart provider

With the useReducer overview over, let’s proceed with defining the CartProvider component.

To avoid overburdening the wrapped provider component that will provide the values to the context, I’ll create a provider wrapper component much like that above with Modal.

Breaking down CartProvider.js into easily digestible steps:

1) Define a default context state that will be the default state for the reducer.

const defaultContext = {
    items: [],
    totalAmount: 0,
    isCartShowing: false
}

2) Set up CartProvider as a wrapper that provides a cartContex as the value to its children components.

const CartContextProvider = ({ children }) => {
    const [cartState, cartDispatcher] = useReducer(cartReducer, defaultContext);

    const addToCartHandler = item => {
        cartDispatcher({ type: 'ADD', data: item });
    }
;
    const removeFromCartHandler = id => {
        cartDispatcher({ type: 'REMOVE', id });
    }

    const showCartHandler = val => cartDispatcher({type: 'CART', val});

    const cartContext = {
        items: cartState.items,
        totalAmount: cartState.totalAmount,
        isCartShowing: cartState.isCartShowing,
        addItem: addToCartHandler,
        removeItem: removeFromCartHandler,
        showCart: showCartHandler
    }

    return (
        <CartContext.Provider value={cartContext}>
            {children}
        </CartContext.Provider>
    )
}

export default CartContextProvider;

cartContex consists of properties and methods. The methods are responsible for pointing to state-updating functions.

In turn, the state-updating functions dispatch the actions that identify what updates take place.

Define the reducer function that takes a current state and updates it based on need.

const cartReducer = (state, action) => {
    if (action.type === 'ADD') {
        let newItems;
        // Check if item exists
        let itemIndex = state.items.findIndex(item => item.id === action.data.id);
        let existingItem = state.items[itemIndex];

        // Update items array
        if (existingItem) {
            // Update only the count of the item
            let updatedItem = {
                ...existingItem,
                count: existingItem.count + action.data.count
            };
            newItems = [...state.items];
            newItems[itemIndex] = updatedItem;
        } else {
            newItems = [...state.items, action.data];
        }

        let newAmount = state.totalAmount + (action.data.price * action.data.count);

        return {
            ...state,
            items: newItems,
            totalAmount: newAmount
        }
    }

    if (action.type === 'REMOVE') {
        let newItems,
            newAmount;

        // Find item
        let itemIndex = state.items.findIndex(item => item.id === action.id);
        let existingItem = state.items[itemIndex];

        if (existingItem) {
            newAmount = state.totalAmount - existingItem.price;

            // Handle item on decrement
            if (existingItem.count <= 1) {
                // Remove item from cart
                newItems = state.items.filter(item => item.id !== action.id);
            } else {
                // Update item count
                let updatedItem = {
                    ...existingItem,
                    count: existingItem.count - 1
                };

                newItems = [...state.items];
                newItems[itemIndex] = updatedItem;
            }
        } else {
            return state;
        }

        return {
            ...state,
            items: newItems,
            totalAmount: newAmount
        }
    }

    if (action.type === 'CART') {
        return {
            ...state,
            isCartShowing: action.val
        };
    }

    return defaultContext;
}

Tip: Note how all these states are part of the larger Cart state!

Adding an item to the cart

On the action type of ADD, we’re adding the item that was clicked to the items array.

This isn’t quite what we want to do because the items array will consist of duplicates of the same item.

For instance, if a user wants two Lattes and, so, adds a Latte twice to the cart, the items array in its current state will consist of two Latte objects.

Cart reducer new state.

Instead of having two or more of the same item, it’ll be best to have one of the same item and update the count property of that item.

Since we’re updating the state based on an existing state, we need to check if the item is already part of the array. If that’s the case, we won’t add the item to the array—we’ll update the existing item.

let newItems;
        // Check if item exists
        let itemIndex = state.items.findIndex( item => item.id === action.data.id);
        let existingItem = state.items[itemIndex];
      
        // Update items array
        if(existingItem) {
            // Update only the count of the item
            let updatedItem = {
                ...existingItem,
                count: existingItem.count + action.data.count
            };
            newItems = [...state.items];
            newItems[itemIndex] = updatedItem; 
        } else {
            newItems = [...state.items, action.data];
        }

Breaking down the code:

  • use findIndex array method to check if an item exists in the items array; if so, itemIndex stores the item’s index
  • use itemIndex to get the entire item object, existingItem
  • make a copy of existingItem, updating the count property to its new value
  • make a copy of the items array so we can update the existing item with updatedItem

Note 👇
With React components, it’s important to maintain array or object immutability to avoid unintended side effects and to ensure proper rendering. To achieve this, create a new array or object with the updated data rather than directly modifying the original data.

Related: 14 ES6 Features You Need To Know To Refresh Your JavaScript

Removing an item from the cart

Similarly to adding an item, we need to handle item removal. It’s important to handle the case of a user clicking on the remove button even though the item is no longer part of the cart.

if (existingItem) {
            newAmount = state.totalAmount - existingItem.price;

            // Handle item on decrement
            if (existingItem.count <= 1) {
                // Remove item from cart
                newItems = state.items.filter(item => item.id !== action.id);
            } else {
                // Update item count
                let updatedItem = {
                    ...existingItem,
                    count: existingItem.count - 1
                };

                newItems = [...state.items];
                newItems[itemIndex] = updatedItem;
            }
        } else {
            return state;
        }

Notice I’m updating newAmount before checking on the item. This is intentional.

In the case the item is removed from the cart, we still need to correctly update the cart’s total amount.

You’ll get an error if you remove the item and then try to locate something that doesn’t exist. I might or might not have learned this the hard way 👉👈

Shopping cart component

Finally, we’re going to tackle the shopping cart itself. This is the content of the Modal wrapper created earlier.

The CartList component takes the data passed from various other components to the cart-context and displays it.

const CartList = () => {
    let { items, totalAmount, addItem, removeItem } = useContext(CartContext);

    return (
        <Modal>
            <div className={s.items_wrapper}>
                <ul>
                    {
                        items.map( (item, i) => {
                            let itemTotalAmnt = (item.count * item.price).toFixed(2);
                            let id = item.id;

                            const addItemHandler = () => {
                                addItem(item);
                            };

                            const rmvItemHandler = () => {
                                removeItem(id);
                            }

                            return(
                                <li key={i} className={s.cartlist_item}>
                                    <span className={s.cartlist_img}><img src={item.image} alt={item.title} /></span>
                                    <span className={s.cartlist_content}>
                                        <span>{item.title}</span><br />
                                        <span>{item.count} × ${item.price}</span>
                                    </span>
                                    <span className={s.cartlist_action}>
                                        <span>${itemTotalAmnt}</span><br />
                                        <CartCount 
                                            count={item.count}
                                            onAddToCart={addItemHandler}
                                            onRemoveFromCart={rmvItemHandler}
                                        /></span>
                                </li>
                            )
                        })
                    }
                </ul>
            </div>
           <div className={s.total_wrapper}>
            <span>Total Amount</span>
            <span>${totalAmount.toFixed(2)}</span>
           </div>
        </Modal>
    )
}

export default CartList;

This is a dynamic component that keeps track of orders and updates in real-time.

Users can add or remove cart items directly from the cart by use of the CartCount controls that make a comeback in this component.

Since CartCount is used in different components yet affects the same state, it’s important to have that state served in the context because it’s managed by the single reducer.

Styling React components with SCSS

Believe it or not, the functional part is over so we need to make the project presentational using styles.

This is my first react project done with SCSS as modules and it was rather simple.

Read: How To Make A Beautiful Accessible Accordion With SCSS

I defined a utils.scss file to hold mixins and functions in the root src directory. To use the defined functions, import the utils in the target file.

Of course, I ran into something because keep in mind we’re still using modules to scope the styles.

For example, you cannot use an SCSS function to define a CSS variable directly within the CSS variable declaration.

CSS variables (custom properties) are defined at the root level of a stylesheet and do not support SCSS or JavaScript functions.

More on CSS variables: An Easy Way To Create A Custom Input Dropdown

However, you can define SCSS variables and then use them to set the values of CSS variables at the root level of your stylesheet.

This allows you to maintain the benefits of variables while generating CSS variables that can be used throughout your stylesheets.

Hey!
In a typical setup of CSS Modules in a Create React App project, styles defined in one SCSS file won’t be directly accessible in another SCSS file unless you explicitly import them or use global CSS classes. This is because CSS Modules provide local scoping by default.

index.scss looks like this:

@import 'utils.scss';

$size: rem(800px);

:root {
  --content: #{$size};
}

// Rest of code here...

While I’m not going to break down all styles used for this project, I want to highlight how you can use CSS properties to control the sizing and aspect ratio of the image.

You can place the <img> inside a container, and then apply CSS styles to achieve the desired effect. This is the usual process for creating a responsive image.

Read: How To Build A Responsive Product Landing Page Using Figma CSS

However, I ran into a small issue with the display of the vertical cold brew image where there was negative space on the sides of the image container.

It’s best practice to size all images similarly, but I’ll let it pass and use what the API provides in raw form for brevity’s sake 😬

I’ll use, instead, the flex property to distribute space within the parent wrapper with the description taking the largest portion.

Using the flex property on images can stretch them when their aspect ratios are not equal, ultimately doing exactly what we want to achieve.

.img {
    max-width: rem(100px);
    max-height: rem(100px);
    overflow: hidden;
    display: flex;
    align-items: center;
    flex: 1;
    border-radius: 50%;

    @media (max-width: 768px) {
      max-width: rem(80px);
      max-height: rem(80px);
    }

    @media (max-width: 425px) {
      display: none;
    }

    img {
      @include respimg;
      object-fit: cover;
    }
  }

All project source files, including stylesheets, are available on the project’s GitHub repo for your reference.

Deploying to GitHub pages

Our project is now complete and ready to deploy. Well, kind of…we must further set the deploy settings.

Before doing anything related to deployment, I’ll use favicon.io to generate all necessary icons for this React project.

Doing so adds custom touches to the default icons create-react-app provides as part of initialization.

With that done, let’s deploy to GitHub pages using the gh-pages package. Don’t forget to set package.json with a homepage and the required deploy scripts before building and launching.

And there you have it—our very own online coffee order app brewed with the power of React.

Who’s feeling like their coding journey just got a whole lot tastier? 😁

Cheers to coding and coffee!

Related Posts