DEV Community

Cover image for Building a shopping cart using React, Redux toolkit
Clarence Gatama Chege
Clarence Gatama Chege

Posted on • Updated on

Building a shopping cart using React, Redux toolkit

In this article, we'll walk through the process of creating an simple e-commerce application with the Fake store API using React redux and react toolkit, with a focus on implementing a shopping cart. By the end of this tutorial, you will have a functional application with the following features:

  1. A product listing page displaying products available for purchase.
  2. The ability to add items to the shopping cart.
  3. A shopping cart page where users can view, update quantity, and remove items from their cart.

Let's get started !!

  1. Create React project using vite:
npm create vite@latest shopping-cart -- --template react
cd shopping-cart
Enter fullscreen mode Exit fullscreen mode
  1. We will create three components: Cart, Navbar, and Shop components.

Project folder structure

3.We will use fakestore api to get products for our project. Below is our initial Shop.jsx:

import React, { useEffect, useState } from "react";
import axios from "axios";

const Shop = () => {
  const [products, setProducts] = useState([]);
  const getProducts = async () => {
    await axios
      .get("https://fakestoreapi.com/products")
      .then((res) => setProducts(res.data))
      .catch((err) => console.log(err));
  };
  useEffect(() => {
    getProducts();
  }, []);
  return (
    <section className="shop">
      {products.map((product) => (
        <article className="card" key={product.id}>
          <img src={product.image} alt="" />
          <div className="details-div">
            <div className="title-price">
              <p>{product.title}</p>
              <p>{product.price}</p>
            </div>
            <button>Add to cart</button>
          </div>
        </article>
      ))}
    </section>
  );
};

export default Shop;

Enter fullscreen mode Exit fullscreen mode

We now have the products set up so we'll dive into redux toolkit.

Redux Toolkit
Redux toolkit is a library that helps us write redux logic. It offers tools that simplify Redux setup and use, reducing boilerplate and enhancing code maintainability.

Redux Store
A redux store is a central repository that stores all the states of an application. Store is made up of slices.
To create our own store we need to install redux toolkit and react redux:

npm install @reduxjs/toolkit react-redux
Enter fullscreen mode Exit fullscreen mode

Create a store.js file inside redux folder inside src folder.

Project folder structure

In store.js we will create a redux store (store) using configureStore provided by reduxjs and export it.

import { configureStore } from "@reduxjs/toolkit";

export const store = configureStore({
  reducer: {},
});

Enter fullscreen mode Exit fullscreen mode

To make the store accessible to our application, we need to wrap our App component inside Provider that allows a prop called store which we will set to our store. This is done inside our main.jsx (could be index.js if you created app using CRA)
Provider is a component that allows Redux store to be made available to all components.

Here is how our main.jsx looks like:

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { store } from "./redux/store.js";
import { Provider } from "react-redux";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

Redux Slice
A slice is a part of the app's state that is managed by a specific reducer and contains actions related to the state.
For our project we'll create productsSlice.js file where we'll use createSlice function to create productsSlice.
createSlice accepts an object with the following parameters:

  • name -defines the slice's name.
  • initialState -sets up the starting state for the slice when the Redux store is first created.
  • reducers - these are functions that handle the logic for updating the state. We will create our first reducer addProductToCart. We will also export actions and reducer created by our createSlice.
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  products: [],
  cart: [],
};

const productsSlice = createSlice({
  name: "product",
  initialState,
  reducers: {
    addProductToCart: (state, action) => {
      state.cart.push(action.payload);
    },
  },
});

export const { addProductToCart } = productsSlice.actions;

export default productsSlice.reducer;

Enter fullscreen mode Exit fullscreen mode

For our productsSlice to be available to the app, we will import reducer created by createSlice inside Store.js and add it to the reducer object of our store.

Here's how our store.js will be:

import { configureStore } from "@reduxjs/toolkit";
import productReducer from "./productsSlice";

export const store = configureStore({
  reducer: {
    products: productReducer,
  },
});
Enter fullscreen mode Exit fullscreen mode

To be able to add a product to cart we will use two react-redux hooks:useSelector and*useDispatch*
useSelector allows components to get(select) part of the state that is required. useDispatch allows components to dispatch actions to the redux store.
Inside Shop.jsx we will import useDispatch from react-redux, invoke it and store in a variable dispatch. Variable dispatch will then be used to dispatch action addProductToCart when button 'Add to cart' is clicked.

Here's our Shop.jsx:

import React, { useEffect, useState } from "react";
import axios from "axios";
import { useDispatch } from "react-redux";
import { addProductToCart } from "../redux/productsSlice";

const Shop = () => {
  const [products, setProducts] = useState([]);
  const dispatch = useDispatch();
  const getProducts = async () => {
    await axios
      .get("https://fakestoreapi.com/products")
      .then((res) => setProducts(res.data))
      .catch((err) => console.log(err));
  };
  useEffect(() => {
    getProducts();
  }, []);
  return (
    <section className="shop">
      {products.map((product) => (
        <article className="card" key={product.id}>
          <img src={product.image} alt="" />
          <div className="details-div">
            <div className="title-price">
              <p>{product.title}</p>
              <p>{product.price}</p>
            </div>
            <button
              onClick={() =>
                dispatch(
                  addProductToCart({
                    id: product.id,
                    title: product.title,
                    price: product.price,
                    image: product.image,
                  })
                )
              }
            >
              Add to cart
            </button>
          </div>
        </article>
      ))}
    </section>
  );
};

export default Shop;

Enter fullscreen mode Exit fullscreen mode

Now that the action is dispatched, the products are successfully added to cart which is part of the state. To display the added products, we will use useSelector inside Cart.jsx to get/extract/select cart which is part of our productsSlice.

Here's our Cart.jsx:

import React from "react";
import { useSelector } from "react-redux";

const Cart = () => {
  const cart = useSelector((state) => state.products.cart);
  console.log(cart);
  return (
    <section className="cart-component">
      {cart.map((product) => (
        <article className="cart-card" key={product.id}>
          <div>
            <img src={product.image} alt="" />
            <button>REMOVE</button>
          </div>
          <div>
            <p>{product.title}</p>
            <div className="button-in-cart">
              <button>-</button>
              <span>{product.quantity}</span>
              <button>+</button>
            </div>
            <p>$:{product.price}</p>
          </div>
        </article>
      ))}
      ;
    </section>
  );
};

export default Cart;

Enter fullscreen mode Exit fullscreen mode

To display the count of products in our navbar, we will also get cart using useSelector inside Navbar.jsx. Cart being an array, we will use its length to get the count of products added to cart.

Here's our Navbar.jsx:

import React from "react";
import { useSelector } from "react-redux";

const Cart = () => {
  const cart = useSelector((state) => state.products.cart);
  console.log(cart);
  return (
    <section className="cart-component">
      {cart.map((product) => (
        <article className="cart-card" key={product.id}>
          <div>
            <img src={product.image} alt="" />
            <button>REMOVE</button>
          </div>
          <div>
            <p>{product.title}</p>
            <div className="button-in-cart">
              <button>-</button>
              <span>{product.quantity}</span>
              <button>+</button>
            </div>
            <p>$:{product.price}</p>
          </div>
        </article>
      ))}
      ;
    </section>
  );
};

export default Cart;

Enter fullscreen mode Exit fullscreen mode

Using redux we have been able to create a store (source of truth 😅) allowing different components to access the state. In the next part we will ensure that when a similar product is clicked a number of times , it's not added to cart as multiple cart products by adding a quantity property. We will also add other actions such as increase quantity of product, decrease quantity of product, remove product and clear cart.

Source code:

React + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:






Top comments (1)

Collapse
 
wearypossum4770 profile image
Stephen Smith

Interesting choice to use axios would the Fetch API have worked as good. When would one be preferred over the other? Also Redux is pretty advanced for this project. I do not see where you have put in the reducers, actions, etc... Is that coming in a later post? Also, remember "Separation of Responsibilities" because we do not want to expose data to a part of the application it is not supposed to have access to. It is good practice because it helps develop a security-centric mindset for coding.