DEV Community

Cover image for Make A Simple Shopping Cart App Using NextJS 13 + Redux
Hòa Nguyễn Coder
Hòa Nguyễn Coder

Posted on

Make A Simple Shopping Cart App Using NextJS 13 + Redux

Now I will make an example of Shopping Cart in Nextjs 13 . Here I have combined with Redux , Redux-Thunk , to handle adding a product to the cart.
If you find this article interesting, please share it, that is to support me.
If you have not seen Redux , then Review my previous articles here:

In this article, I also apply the knowledge from the articles about Redux that I have done, so there is not much explanation, because most of React is similar to NextJS. The only thing is that React runs rendering on the client, while Nextjs renders on the server, so it supports us in SEOing keywords, etc.
The layout of today's content is as follows:

  • app/_assets/images : Used to save images

  • app/_components/Header.tsx: Configure the header interface, display the number of product indexes in the shopping cart

  • app/_libs/index.ts : Libraries that need to be installed

  • app/_redux/actions/index.js: Configure actions for Redux , when we dispatch an action it will go to Reducers for processing, say Reducers it takes care of data changes, then it updates to Stores , so we need to configure the actions to make them easier to understand and maintain

  • app/_redux/reducers/index.js: Place to receive incoming actions. Reducers will identify actions to process. Once processed, it will update to Stores

  • app/_redux/stores/index.js: Where to store system states. It is the place to manage States, making it easy to retrieve and use anywhere in the Component

  • app/_redux/redux-provider.js : It uses the Provider component from the react-redux library to provide stores to child components via the store prop . Child components are passed in via the children prop .

  • app/_redux/provider.tsx : This component is used to provide Redux store functionality to its child components. The PropsWithChildren type is used to define prop children

  • app/_types/index.ts: Configure interfaces in typescript , use it to format data types, making it easier to check errors

  • app/(route)/api/router.ts: Set up the api route , to send a request to get all products, for example: http://localhost:3000/api/products

  • app/(route)/api/[id]/route.ts: Set up API, send with product ID , to get information about that product. For example: http://localhost:3000/api/products/12

  • app/(route)/cart/page.tsx: Set up the interface to display products that the user has purchased in the cart ( carts ). We take the products in the shopping cart at Redux's Stores

  • app/(route)/product/page.tsx : In this component we need to display all products obtained from the request / api/products/route.ts action . For example: http://localhost:3000/api/products

  • app/(route)/product/[id]/page.tsx: In this component we display the product by ID . From the action request api/products/[id]/route.ts . For example: http://localhost:3000/api/products/12

  • app/page.tsx: Configure display component

  • app/layout.tsx: Configure the system layout, and also configure Providers in React-Redux to be able to use Redux in Components

  • .env: Create an .env file in the system's total directory, set environment variables for it. For example: PATH_URL_BACKEND = https://dummyjson.com

Okay, that's it, now let's go through the files above, everyone can see them at: Github

Demo: If you find it interesting, please subscribe to support Hoa Nguyen Coder Youtube channel

Link Demo: https://make-a-simple-shopping-cart-app-using-nextjs13-redux.vercel.app//

Here I use the API: https://dummyjson.com , I find it very good, you can use it

import { NextRequest, NextResponse } from 'next/server'

export async function GET() {
  const res = await fetch(process.env.PATH_URL_BACKEND+'/products', {
    headers: {
      'Content-Type': 'application/json',
    },
  })
  const result = await res.json()
  return NextResponse.json({ result })
}
Enter fullscreen mode Exit fullscreen mode
import { NextRequest, NextResponse } from "next/server"
export async function GET(request : NextRequest,{ params }: { params: { id: number } }) {
    const res = await fetch(process.env.PATH_URL_BACKEND+`/products/${params.id}`, {
      next: { revalidate: 10 } ,
      headers: {
        'Content-Type': 'application/json',
      },
    })
    const result = await res.json()
    return NextResponse.json(result)
  }  
Enter fullscreen mode Exit fullscreen mode

Okay, next we will set up Redux

  • app/_redux/actions/index.js : Set up and install actions, helping us easily call and distpatch an action
export const INCREASE_QUANTITY = "INCREASE_QUANTITY";
export const DECREASE_QUANTITY = "DECREASE_QUANTITY";
export const GET_NUMBER_CART = "GET_NUMBER_CART";
export const ADD_CART = "ADD_CART";
export const UPDATE_CART = "UPDATE_CART";
export const DELETE_CART = "DELETE_CART";

/*GET NUMBER CART*/
export function GetNumberCart() {
  return {
    type: "GET_NUMBER_CART",
  };
}

export function AddCart(payload) {
  return {
    type: "ADD_CART",
    payload,
  };
}
export function UpdateCart(payload) {
  return {
    type: "UPDATE_CART",
    payload,
  };
}
export function DeleteCart(payload) {
  return {
    type: "DELETE_CART",
    payload,
  };
}

export function IncreaseQuantity(payload) {
  return {
    type: "INCREASE_QUANTITY",
    payload,
  };
}
export function DecreaseQuantity(payload) {
  return {
    type: "DECREASE_QUANTITY",
    payload,
  };
}
Enter fullscreen mode Exit fullscreen mode
  • app/_redux/reducers/index.js : In this file, we need to identify actions to process them, and update data to Stores
import { combineReducers } from "redux";
import {
  GET_NUMBER_CART,
  ADD_CART,
  DECREASE_QUANTITY,
  INCREASE_QUANTITY,
  DELETE_CART,
} from "../actions";
const initProduct = {
  numberCart: 0,
  Carts: [],
};

function todoProduct(state = initProduct, action) {
  switch (action.type) {
    case GET_NUMBER_CART:
      return {
        ...state,
      };
    case ADD_CART:
      if (state.numberCart == 0) {
        let cart = {
          id: action.payload.id,
          quantity: 1,
          name: action.payload.title,
          image: action.payload.thumbnail,
          price: action.payload.price,
        };
        state.Carts.push(cart);
      } else {
        let check = false;
        state.Carts.map((item, key) => {
          if (item.id == action.payload.id) {
            state.Carts[key].quantity++;
            check = true;
          }
        });
        if (!check) {
          let _cart = {
            id: action.payload.id,
            quantity: 1,
            name: action.payload.title,
            image: action.payload.thumbnail,
            price: action.payload.price,
          };
          state.Carts.push(_cart);
        }
      }
      return {
        ...state,
        numberCart: state.numberCart + 1,
      };
    case INCREASE_QUANTITY:
      state.numberCart++;
      state.Carts[action.payload].quantity++;

      return {
        ...state,
      };
    case DECREASE_QUANTITY:
      let quantity = state.Carts[action.payload].quantity;
      if (quantity > 1) {
        state.numberCart--;
        state.Carts[action.payload].quantity--;
      }

      return {
        ...state,
      };
    case DELETE_CART:
      let quantity_ = state.Carts[action.payload].quantity;
      console.log(quantity_);
      return {
        ...state,
        numberCart: state.numberCart - quantity_,
        Carts: state.Carts.filter((item) => {
          return item.id != state.Carts[action.payload].id;
        }),
      };
    default:
      return state;
  }
}
const ShopApp = combineReducers({
  _todoProduct: todoProduct,
});
export default ShopApp;
Enter fullscreen mode Exit fullscreen mode

I have explained the above code in articles about Redux. You can review it in the Redux section of the website.

  • app/_redux/stores/index.js : call reducers into the store
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import ShopApp from '../reducers/index'
const store =  createStore(ShopApp,applyMiddleware(thunkMiddleware));
export default store;
Enter fullscreen mode Exit fullscreen mode
  • app/_reudx/redux-provider.js : Call Provider trong react-redux
"use client";
import store from "./stores";
import { Provider } from "react-redux";
export default function ReduxProvider({ children }) {
  return (
      <Provider store={store}>
          {children}
      </Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • app/_redux/Provider.tsx :
"use client";
import { PropsWithChildren } from "react";
import ReduxProvider from "./redux-provider";
export default function Providers({ children }: PropsWithChildren<any>) {
    return (
        <ReduxProvider>
            {children}
        </ReduxProvider>
    );
}
Enter fullscreen mode Exit fullscreen mode

Redux setup is complete, but to use it we need to reconfigure our layout

  • app/layout.tsx :
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import Providers from './_redux/provider'
import Header from './_components/Header'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
  title: 'Create a simple example Cart in NextJS 13',
  description: 'Create a simple example Cart in NextJS 13',
}
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
        <html lang="en">
          <body className={inter.className}>
               <Providers>
                    <Header />
                    {children}
                </Providers>
          </body>
        </html>
  )
}
Enter fullscreen mode Exit fullscreen mode
  • app/page.tsx :
import ProductPage from './(routes)/product/page'
export default function Home() {
  return (
   <div className='w-full max-w-6xl m-auto'>
       <ProductPage />
   </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

So we can use it, now we just need to go to the component and call and use it.
Next, if you are diligent, set up data types in typescript, to help us easily check errors and bind data types. reasonable data

  • app/_types/index.ts :
export interface IProduct {
    id: number
    title: string
    description: string,
    brand:string,
    price: number,
    thumbnail: string,
    images: string[],
}

Enter fullscreen mode Exit fullscreen mode
  • app/_libs/index.ts : export a fetch function to call in components
export const fetcher = (url: string) => fetch(url).then((res) => res.json());
Enter fullscreen mode Exit fullscreen mode

Okay, now this is the step where we call and use them

  • app/_components/Header.tsx :
'use client'
import Link from 'next/link'
import React from 'react'
import { useSelector } from 'react-redux'
import icon_cart from "@/app/_assets/images/icons8-cart-80.png";
import Image from 'next/image';
export default function Header() {
  const  numberCart  = useSelector((state: any) => state._todoProduct.numberCart);
  return (
    <div className='w-full p-5 bg-gray-300'>
       <div className='w-full max-w-6xl m-auto px-4 flex flex-row items-center justify-between'>
          <Link href={'/'} className='font-bold text-xl'>Hoa Dev <br/> <span className='text-red-500 text-sm'>https://hoanguyenit.com</span></Link>
            <Link href='/cart' className='bg-white p-2 block rounded-md'>
                <div className='flex flex-row gap-2'><Image src={icon_cart} alt="cart" width={25} height={25} /> Cart : <span className='font-bold text-red-500 inline-block'>
                      {numberCart}
                  </span></div>
            </Link>
       </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The above code has useSelector. If you want to send an action, use useDispatch
useSelector : helps us get data stored in the store. The code below will get the total number of products in the cart.

const  numberCart  = useSelector((state: any) => state._todoProduct.numberCart);
Enter fullscreen mode Exit fullscreen mode

useDispatch : helps us dispatch an action, for example:

dispatch(AddCart(product))
//or
dispatch({type:'ADD_CART',payload:product});

Enter fullscreen mode Exit fullscreen mode
  • app/(route)/product/page.tsx : The snippet below, we just need to request the api to get the data and display it
"use client"
import Image from 'next/image'
import Link from 'next/link'
import useSWR from 'swr';
import { fetcher } from '@/app/_libs';
import { IProduct } from '@/app/_types';
export default function ProductPage() {
    const { data , error, isLoading } = useSWR<any>(
        `/api/products`,
        fetcher
      );
    if (error) return <div>Failed to load</div>;
    if (isLoading) return <div>Loading...</div>;
    if (!data) return null;
  return (
    <div className='w-full'>
        <ul className='flex flex-wrap mt-4'>
            {
                data && data.result.products.map((product: IProduct) => {
                    return (
                        <li key={product.id} className="w-full sm:w-1/2 md:w-1/3 xl:w-1/4 p-4">
                            <div className="my-2 bg-white rounded-[20px] overflow-hidden relative sm:h-auto md:h-[380px] hover:shadow-md  border-gray-500/20 border-[1px]">
                                    <Link href={`/product/${product.id}`}><Image className="w-full block h-[230px] sm:h-auo border-[1px] border-gray-300" src={product.thumbnail} alt=""  width={200} height={120} /></Link>
                                    <div className="p-4">
                                        <h2 className="capitalize text-xl sm:text-[14px] md:text-[16px] font-bold"><Link href={`/product/${product.id}`}>{product.title}</Link></h2>
                                    </div>
                                    <div className="w-full sm:relative md:absolute bottom-0 flex justify-between items-center border-t-[1px] border-gray-200 py-2">
                                        <ul className="pl-4">
                                            <li className="inline-block px-1"><Link href="react"><span className="inline-block text-[12px]">#{product.brand}</span></Link></li>
                                        </ul>
                                        <div className="pr-4">
                                            <span className="text-[14px] font-bold"><i className="fas fa-eye pr-2"></i>Price: {product.price}</span>
                                        </div>
                                    </div>
                            </div>
                        </li>
                    )
                })

            }
        </ul>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
  • app/(route)/product/[id]/page.tsx : request api with ID, to get product information by that ID
"use client"
import Image from 'next/image'
import { useDispatch } from 'react-redux'
import useSWR from 'swr';
import { fetcher } from '@/app/_libs';
import { AddCart } from '@/app/_redux/actions'
export default function ProductDetailPage({ params }: { params: { id: number } }) {
    const dispatch = useDispatch();
    const { data : product , error, isLoading } = useSWR<any>(
        `/api/products/${params.id}`,fetcher
    );
    if (error) return <div>Failed to load</div>;
    if (isLoading) return <div>Loading...</div>;
    if (!product) return null;
  return (
    <div className='w-full max-w-[400px] m-auto flex flex-col justify-center'>
       <div className="w-full mt-4">
            <Image src={product?.thumbnail} alt={product?.title} width={400} height={400}/>
            <div className='w-full mt-2'>
                <h1 className='font-bold text-2xl text-red-500'>{product?.title}</h1>
                <p className='text-gray-500'>{product?.description}</p>
                <p className='text-gray-500'>Price: ${product?.price}</p>
                <button className='bg-yellow-400 px-4 py-2 text-white mt-1' onClick={() => dispatch(AddCart(product))}>Add to Cart</button>
            </div>
       </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The above code uses a dispatch(AddCart(product)) , which helps send an action to Reducers for processing, after processing, updating carts in Stores .

  • app/(route)/cart/page.tsx : List products in the cart (carts) for users to see
"use client"
import { DecreaseQuantity, DeleteCart, IncreaseQuantity } from "@/app/_redux/actions";
import { IProduct } from "@/app/_types";
import Image from "next/image";
import React from "react";

import { useSelector, useDispatch } from 'react-redux';
export default function CartPage() {
  const dispatch = useDispatch();
  const items = useSelector((state: any) => state._todoProduct);
  //  console.log(items)
  const ListCart: any[] = [];
  let TotalCart=0;
  Object.keys(items.Carts).forEach(function(item){
      TotalCart+=items.Carts[item].quantity * items.Carts[item].price;
      ListCart.push(items.Carts[item]);
  });
  return (
    <div className="w-full max-w-4xl m-auto">
      <table className="w-full table-auto">
        <caption className="caption-top text-left font-bold py-5">
             Carts
        </caption>
        <thead>
          <tr>
            <td className="border border-slate-300 p-2"></td>
            <th className="border border-slate-300 p-2">Image</th>
            <th className="border border-slate-300 p-2">Title</th>
            <th className="border border-slate-300 p-2">Price</th>
            <th className="border border-slate-300 p-2">Quantity</th>
            <th className="border border-slate-300 p-2">Total Price</th>
          </tr>
        </thead>
        <tbody>
          {
            ListCart && ListCart.map((cart: any,key : number) => {
              return (
                <tr key={cart.id}>
                  <td className="border border-slate-300 p-2"><button className="bg-red-500 w-10 text-center text-xl px-2 py-1 text-white ml-5"  onClick={()=>dispatch(DeleteCart(key))}>X</button></td>
                  <td className="border border-slate-300 p-2">
                    <Image src={cart.image} alt={cart.name} width={150} height={150} />
                    </td>
                  <td className="border border-slate-300 p-2">{cart.name}</td>
                  <td className="border border-slate-300 p-2">{cart.price}</td>
                  <td className="border border-slate-300 p-2">
                    <div className="flex flex-row gap-2 justify-center">
                          <span className="text-xl px-2 py-1 text-black font-bold cursor-pointer" onClick={() => dispatch(DecreaseQuantity(key))}>-</span>
                          <span className="bg-gray-400 w-10 text-center text-xl px-1 py-1 text-white font-bold">{cart.quantity}</span>
                          <span className="text-xl px-2 py-1 text-black cursor-pointer" onClick={() => dispatch(IncreaseQuantity(key))} >+</span>

                    </div>
                  </td>
                  <td className="border border-slate-300 p-2">{(cart.quantity * cart.price).toLocaleString('en-US')} $</td>

                </tr>
              )
            })
          }

        </tbody>
      </table>
      <div className="w-full">
        <div className="w-full mt-4">
            <h1 className="font-bold text-2xl text-red-500">Total : {Number(TotalCart).toLocaleString('en-US')} $</h1>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The code above takes the products in the cart, calculates the price, saves them to the ListCart array , then runs a loop to display them.

const items = useSelector((state: any) => state._todoProduct);
  const ListCart: any[] = [];
  let TotalCart=0;
  Object.keys(items.Carts).forEach(function(item){
      TotalCart+=items.Carts[item].quantity * items.Carts[item].price;
      ListCart.push(items.Carts[item]);
  });
Enter fullscreen mode Exit fullscreen mode

Set up the following dispatch actions:

dispatch(DecreaseQuantity(key)) : send action to handle decreasing the quantity of a current product by key
dispatch(IncreaseQuantity(key)) : send action to handle increasing an existing product by key

Okay that's it, If you find it interesting, please share this article! Coming to everyone!
The Article : Make A Simple Shopping Cart App Using NextJS 13 + Redux

Demo Image:

Make A Simple Shopping Cart App Using NextJS 13 + Redux - hoanguyenit.com

Make A Simple Shopping Cart App Using NextJS 13 + Redux - hoanguyenit.com

Make A Simple Shopping Cart App Using NextJS 13 + Redux - hoanguyenit.com

Top comments (0)