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
- 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
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
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
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
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
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 (
{children}
);
}
+ **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 :
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 :
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 :
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
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 :
'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.
const numberCart = useSelector((state: any) => state._todoProduct.numberCart);
useDispatch : helps us dispatch an action, for example:
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
"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
"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
"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.
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
Demo Image:
Top comments (1)
hello a beginner here I tried your code but came into various errors so I modified it and the initial error of cannot read property of type (I might have mistyped the error but it was something similar) where it cannot read "action.type" from _redux/action/index.js so I moved all the code from switch case to different functions in action/index.js and made an object of functions in reducer/index.js and now it says that it cannot read property id from AddCart function. I renamed it to Prod_Id as well thinking "id" might be a reserved keyword but it still didn't work and I can't find anywhere in your code that what is payload please help