Next.js & Node.js (Phần 3): Tối Ưu Trải Nghiệm Với useSWR, useEffect Và Quản Lý State
Nếu ở phần trước chúng ta đã làm quen với Server Components, thì hôm nay chúng ta sẽ chuyển sang Client Components để xử lý các tác vụ đòi hỏi tính tương tác cao như: xem chi tiết sản phẩm, sắp xếp giá và lọc theo danh mục.
Bài 1: Trang Chi Tiết Sản Phẩm Với useSWR
useSWR là một thư viện cực kỳ mạnh mẽ giúp quản lý cache, tự động fetch lại dữ liệu và giữ cho giao diện luôn đồng bộ với server mà không cần load lại trang.
1. Cấu hình Backend (Node.js)
Đầu tiên, hãy cấp quyền truy cập cho Frontend bằng cách cài đặt thư viện cors:
npm install cors
Trong app.js của backend, hãy thêm:
const cors = require('cors');
app.use(cors());
Và viết API lấy chi tiết một sản phẩm theo ID:
router.get('/productdetail/:id', async(req, res) => {
let id = new ObjectId(req.params.id);
const db = await connectDb();
const product = await db.collection('products').findOne({_id: id});
product ? res.status(200).json(product) : res.status(404).json({message: "Không tìm thấy"});
});
2. Hiển thị phía Frontend
Tại trang chitietsanpham/[id]/page.jsx, chúng ta sử dụng cơ chế Dynamic Routes:
"use client";
import useSWR from 'swr';
const fetcher = (...args) => fetch(...args).then((res) => res.json());
export default function DetailPage({ params }) {
const { data: product, error, isLoading } = useSWR(
`http://localhost:3000/productdetail/${params.id}`,
fetcher,
{ refreshInterval: 6000 } // Tự động cập nhật sau 6 giây
);
if (error) return <div className="alert alert-danger">Lỗi load dữ liệu.</div>;
if (isLoading) return <div>Đang tải...</div>;
return (
<div className="container mt-5">
<div className="row shadow-lg p-3 mb-5 bg-white rounded">
<div className="col-md-6">
<img className="img-fluid rounded" src={`http://localhost:3000/img/${product.image}`} alt={product.name} />
</div>
<div className="col-md-6">
<h2 className="text-primary">{product.name}</h2>
<h4 className="text-danger">Giá: {product.price.toLocaleString()} VNĐ</h4>
<p className="mt-3">{product.description}</p>
<div className="d-flex align-items-center gap-3 mt-4">
<input className="form-control w-25" type="number" defaultValue="1" min="1" />
<button className="btn btn-success">Thêm vào giỏ hàng</button>
</div>
</div>
</div>
</div>
);
}
Bài 2 & 3: Quản Lý Danh Sách Với useEffect Và State
Khi bạn muốn thực hiện các tính năng như Sắp xếp giá mà không muốn gọi lại API liên tục, việc sử dụng useState để quản lý dữ liệu cục bộ là giải pháp tối ưu.
Logic Sắp Xếp Dữ Liệu
Chúng ta sẽ lưu trữ danh sách sản phẩm vào một state. Khi người dùng thay đổi lựa chọn trong thẻ <select>, một hàm xử lý sẽ được kích hoạt để sắp xếp lại mảng sản phẩm.
'use client';
import { useState, useEffect } from 'react';
import ProductCard from '../component/ProductCard';
export default function ProductPage() {
const [products, setProducts] = useState([]);
const [sortOption, setSortOption] = useState('asc');
useEffect(() => {
fetch('http://localhost:3000/products')
.then(res => res.json())
.then(data => setProducts(data));
}, []);
// Hàm xử lý sắp xếp
const getSortedProducts = () => {
return [...products].sort((a, b) => {
return sortOption === 'asc' ? a.price - b.price : b.price - a.price;
});
};
return (
<div className="container my-4">
<div className="d-flex justify-content-between align-items-center mb-4">
<h5 className="text-success fw-bold">DANH SÁCH SẢN PHẨM</h5>
<select className="form-select w-auto" onChange={(e) => setSortOption(e.target.value)}>
<option value="asc">Giá tăng dần ↑</option>
<option value="desc">Giá giảm dần ↓</option>
</select>
</div>
<div className="row">
<ProductCard data={getSortedProducts()} />
</div>
</div>
);
}
Bài 4: Thử Thách – Lọc Dữ Liệu Theo Danh Mục (Dành cho điểm 10)
Để hoàn thiện bài Lab này, bạn hãy tự thực hiện tính năng Lọc (Filter). Gợi ý logic:
Tạo một state selectedCategory để lưu ID danh mục đang chọn.
Sử dụng hàm .filter() trên mảng products để chỉ hiển thị các sản phẩm có categoryId khớp với selectedCategory.
Nếu người dùng chọn "Hiển thị tất cả", hãy trả về toàn bộ mảng ban đầu.
Kết luận
Lab 3 đã giúp chúng ta hiểu rõ sự khác biệt giữa việc render dữ liệu tĩnh và xử lý dữ liệu động linh hoạt trên Client. Việc kết hợp giữa SWR để lấy dữ liệu chi tiết và useState để thao tác danh sách sẽ giúp ứng dụng của bạn mượt mà như một trang thương mại điện tử thực thụ.
All rights reserved