Make A Simple Shopping Cart App Using NextJS 13 + Redux
Tags: webdev, beginners, nextjs, react
Published: 2023-10-08 08:33:53

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:
+ [Build a Shopping Cart with React + Redux](https://dev.to/skipperhoa/build-a-shopping-cart-with-react-redux-1n97)
+ [Create A Simple React With Redux](https://dev.to/skipperhoa/create-a-simple-react-with-redux-55je)

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<any> 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](https://github.com/skipperhoa/make-a-simple-shopping-cart-app-using-nextjs13-redux)

Demo: If you find it interesting, please subscribe to support  [Hoa Nguyen Coder   Youtube channel ](https://www.youtube.com/channel/UCBOZRctXJSg9YNLyddedASg)

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

{% youtube cTFMwjYQqVE%}

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

+ **app/(route)/api/router.ts** : Build GET method. Help us request _http://localhost:3000/api/products_ , it will call and get all products from the api: _https://dummyjson.com/products_

```javascript
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 })
}
```

+ **app/(route)/api/[id]/route.ts** : Get the product according to the ID we insert into the link, request _http://localhost:3000/api/product/[id]_   , it will get the product at api: _https://dummyjson.com/products/12_

```javascript
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)
  }  
```
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

```javascript
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,
  };
}
```
+ **app/_redux/reducers/index.js** : In this file, we need to identify actions to process them, and update data to Stores 

```javascript
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;
```
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

```javascript
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import ShopApp from '../reducers/index'
const store =  createStore(ShopApp,applyMiddleware(thunkMiddleware));
export default store;
```

+ **app/_reudx/redux-provider.js** : Call Provider trong react-redux
```javascript
"use client";
import store from "./stores";
import { Provider } from "react-redux";
export default function ReduxProvider({ children }) {
  return (
      <Provider store={store}>
          {children}
      </Provider>
  );
}
```
+ **app/_redux/Provider.tsx** : 

```javascript
"use client";
import { PropsWithChildren } from "react";
import ReduxProvider from "./redux-provider";
export default function Providers({ children }: PropsWithChildren<any>) {
    return (
        <ReduxProvider>
            {children}
        </ReduxProvider>
    );
}
```
Redux setup is complete, but to use it we need to reconfigure our layout
+ **app/layout.tsx** :

```javascript
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>
  )
}
```
+ **app/page.tsx** :

```javascript
import ProductPage from './(routes)/product/page'
export default function Home() {
  return (
   <div className='w-full max-w-6xl m-auto'>
       <ProductPage />
   </div>
  )
}
```
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** :

```javascript
export interface IProduct {
    id: number
    title: string
    description: string,
    brand:string,
    price: number,
    thumbnail: string,
    images: string[],
}

```
+ **app/_libs/index.ts** : export a fetch function to call in components 

```javascript
export const fetcher = (url: string) => fetch(url).then((res) => res.json());
```
Okay, now this is the step where we call and use them
+ **app/_components/Header.tsx** : 

```javascript
'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>
  )
}
```
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.

```javascript
const  numberCart  = useSelector((state: any) => state._todoProduct.numberCart);
```
**useDispatch** : helps us dispatch an action, for example:

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

```
+ **app/(route)/product/page.tsx** : The snippet below, we just need to request the api to get the data and display it

```javascript
"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>
  )
}
```
+ **app/(route)/product/[id]/page.tsx** : request api with ID, to get product information by that ID

```javascript
"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>
  )
}
```
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

```javascript
"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>
  );
}
```
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.

```javascript
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]);
  });
```
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](https://hoanguyenit.com/make-a-simple-shopping-cart-app-using-nextjs13-redux.html)

Demo Image:

![Make A Simple Shopping Cart App Using NextJS 13 + Redux - hoanguyenit.com](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/co7fkjg0kd4ch1w3949g.png)

![Make A Simple Shopping Cart App Using NextJS 13 + Redux - hoanguyenit.com](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ojtqonjes2r6348xu0w.png)

![Make A Simple Shopping Cart App Using NextJS 13 + Redux - hoanguyenit.com](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k79qf32jbns2je10mk30.png)