Manage State Like Hook on NextJs 13 with Zustand
React State Management
--
Zustand is State Management to help manage state like get or change value that you can access through entire component without prop drilling.
We can try to build add to cart function, when Add To Cart
clicked increase item on number on <CartButton/>
component.
Step 1: Install Dependency
- Zustand
npm install zustand
2. Heroicon React
npm install @heroicons/react
Step 2: Build UI
- Modify
/app/layout.tsx
import Navbar from "@/components/Navbar"
import './globals.css'
import { Montserrat } from 'next/font/google'
const font = Montserrat({
weight: "500",
preload: false
})
export const metadata = {
title: 'LearnThink',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={`antialiased bg-slate-950 text-white ${font.className}`}>
<Navbar/>
<main className="mx-auto max-w-6xl">
{children}
</main>
</body>
</html>
)
}
- Create
<Navbar/>
component:/components/Navbar.tsx
import Link from "next/link";
import LinkMenu from "./link/Menu";
import CartButton from "./CartButton";
export default function Navbar() {
return <nav>
<div className="mx-auto max-w-6xl flex justify-between items-center text-sm text-slate-100 py-4">
<Link href="/">
<div className="uppercase font-bold">Learn<span className="text-teal-500">Thing</span></div>
</Link>
<div className="flex gap-2">
<LinkMenu href="/product" label="Product" isNew/>
</div>
<div className="flex gap-1 items-center">
<CartButton/>
</div>
</div>
</nav>
}
- Create
<LinkMenu/>
component:/components/link/Menu.tsx
import Link from "next/link";
type Props = {
href: string,
label?: string,
isNew?: boolean,
}
export default function LinkMenu({
href,
label,
isNew
}: Props) {
return (
<Link href={href}
className="text-sm text-slate-700 px-2 py-1 rounded-md
hover:bg-slate-900 hover:text-slate-500
transition duration-200 ease-in-out">
<span className="relative">
{label}
{isNew && <NewItem/>}
</span>
</Link>
);
}
const NewItem = () => <span className="absolute bg-orange-400 w-2 h-2 rounded-full"></span>
- Create
<CartButton/>
component:/components/CartButton.tsx
'use client';
import { ShoppingCartIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
export default function CartButton() {
return (
<Link href="/cart" className="p-2 rounded-md hover:bg-slate-900 text-slate-700 hover:text-slate-300 relative transition duration-200 ease-in-out">
<ShoppingCartIcon className="w-6 h-6" strokeWidth={2} />
<Label item={0} />
</Link>
)
}
const Label: React.FC<{ item: number }> = ({ item }) => {
if (item === 0) return <></>
return (
<span className="absolute top-0 right-0 w-4 h-4 bg-red-400 text-white font-semibold text-xs rounded-full grid place-content-center">
{item}
</span>
)
}
- Create new Product page
/app/product/page.tsx
import ProductCard from "@/components/ProductCard";
export type Product = {
id: number,
name: string,
price: number
}
export default function Page() {
const products: Product[] = [
{
id: 1,
name: "Americano",
price: 40
},
{
id: 2,
name: "Expresso",
price: 20
},
{
id: 3,
name: "Arabica",
price: 10
}
];
return (<>
<h1 className="font-semibold text-slate-200 text-2xl border-b pb-4 border-b-slate-700">Products</h1>
<div className="text-sm pt-4 flex gap-4">
{products.map(product =>
<ProductCard key={product.id}
id={product.id}
name={product.name}
price={product.price}/>
)}
</div>
</>)
}
- Create component ProductCard
/components/ProductCard.tsx
'use client'
import Image from "next/image";
import CoffeeImage from "@/public/product-image/coffee.png";
import { formatNumber } from "@/utils/format";
type ProductCartProps = {
id: number,
name: string
price: number,
}
export default function ProductCard({
id, name, price
}: ProductCartProps) {
const {add: handleAddToCart} = useCartStore();
return (
<div className="border p-3 rounded-xl border-slate-700">
<div className="bg-gray-300 rounded-md mb-2">
<Image src={CoffeeImage} alt="coffee" className="w-[180px] h-[180px] rounded object-cover" />
</div>
<h2 className="text-slate-400">{name}</h2>
<h2 className="font-semibold text-green-400">$ {formatNumber(price)}</h2>
<button className="mt-4 font-semibold text-sm bg-slate-100 text-slate-800 rounded-md py-2 text-center w-full">
Add To Cart
</button>
</div>
)
}
- Create utils for formating number
/utils/format.ts
export function formatNumber(number: number) {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}
Step 3: Implement Zustand
- Create store for cart
/store/zustand.ts
import { create } from 'zustand'
type CartStore = {
cart: number,
add: () => void,
remove: () => void,
removeAll: () => void
}
export const useCartStore = create<CartStore>((set) => ({
cart: 0,
add: () => set((state) => ({ cart: state.cart + 1 })),
remove: () => set((state) => ({ cart: state.cart - 1 })),
removeAll: () => set({ cart: 0 }),
}));
Create new state for manage the cart
with create
function from zustand
and add some function to the state like add, remove and removeAll.
2. Modify <ProductCard/>
component /components/ProductCard.tsx
:
'use client'
...
import { useCartStore } from "@/store/zustand";
export default function ProductCard() {
const {add: handleAddToCart} = useCartStore();
return (
<div>
...
<button onClick={handleAddToCart}>
Add To Cart
</button>
</div>
)
}
Import useCartStore
that we created earlier, use add
function from useCartStore
rename with handleAddToCart
and trigger whenonClick
event.
3. Modify <CartButton/>
component /components/CartButton.tsx
'use client';
import { useCartStore } from "@/store/zustand";
export default function CartButton() {
const { cart } = useCartStore();
return (
<Link ...>
...
<Label item={cart} />
</Link>
)
}
Import useCartStore
, get cart
value from useCartStore
and change item
value with cart