Manage State Like Hook on NextJs 13 with Zustand

React State Management

Nemesis
4 min readAug 28, 2023
Zustand

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.

Product Page with Zustand

Step 1: Install Dependency

  1. Zustand
npm install zustand

2. Heroicon React

npm install @heroicons/react

Step 2: Build UI

  1. 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

  1. 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

--

--