Data Fetching trong App Router
Bài 11 – Các kỹ thuật lấy dữ liệu trong App Router và cơ chế caching
1. Giới thiệu về Data Fetching trong App Router
App Router của Next.js giới thiệu mô hình data fetching hoàn toàn mới. Thay vì dùng các hàm như getServerSideProps hoặc getStaticProps, Next.js sử dụng fetch() theo chuẩn web, kết hợp cơ chế caching thông minh để tự động quyết định SSR, SSG hoặc ISR.
2. Nội dung chính
2.1. Data fetching bằng fetch() trong Server Components
Next.js mở rộng Web APIs để hoạt động tối ưu trong Server Components:
// Server Component mặc định - tự động SSG
export default async function Page() {
const data = await fetch('https://api.example.com/data');
const posts = await data.json();
return <div>{posts.map(post => <h2>{post.title}</h2>)}</div>;
}
2.2. Cách hoạt động của cache: force-cache, no-store, revalidate
Next.js cung cấp các tùy chọn caching linh hoạt:
// SSG với cache (mặc định)
const res1 = await fetch('https://api.example.com/posts', {
cache: 'force-cache' // Hoặc không cần specify
});
// SSR với no-store
const res2 = await fetch('https://api.example.com/realtime', {
cache: 'no-store'
});
// ISR với revalidate
const res3 = await fetch('https://api.example.com/news', {
next: { revalidate: 60 } // 60 giây
});
2.3. Phân biệt data fetching ở Server Component và Client Component
Server Component:
- Fetch dữ liệu trước khi render
- Không gửi JavaScript fetch code xuống client
- Phù hợp cho SEO và performance
Client Component:
- Fetch dữ liệu sau khi component mount
- Cần khai báo
"use client" - Phù hợp cho tương tác và realtime data
// Server Component
export default async function ServerPosts() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return <div>{posts.length} bài viết</div>;
}
// Client Component
"use client";
import { useEffect, useState } from 'react';
export default function ClientPosts() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(setPosts);
}, []);
return <div>{posts.length} bài viết</div>;
}
2.4. Streaming và Suspense
Streaming cho phép render phần của page mà không cần chờ tất cả data:
import { Suspense } from 'react';
import Posts from './Posts';
import Sidebar from './Sidebar';
export default function Page() {
return (
<div>
<h1>My Blog</h1>
<Suspense fallback={<div>Loading posts...</div>}>
<Posts />
</Suspense>
<Suspense fallback={<div>Loading sidebar...</div>}>
<Sidebar />
</Suspense>
</div>
);
}
2.5. Ưu điểm của cơ chế data fetching trong App Router
- Đơn giản: Dùng fetch() chuẩn thay vì custom functions
- Flexible: Dễ dàng chuyển đổi giữa SSR/SSG/ISR
- Performance: Tự động caching và deduplication
- Developer Experience: TypeScript support tốt hơn
3. Ví dụ thực tế
3.1. Data fetching cơ bản (SSG hoặc cached fetch)
// app/products/page.tsx – SSG mặc định
import { notFound } from 'next/navigation';
interface Product {
id: string;
name: string;
price: number;
description: string;
}
export default async function ProductsPage() {
try {
const res = await fetch("https://api.example.com/products");
if (!res.ok) {
throw new Error('Failed to fetch products');
}
const products: Product[] = await res.json();
return (
<div className="products-container">
<h1 className="text-3xl font-bold mb-6">Danh sách sản phẩm</h1>
<div className="products-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
<h3 className="text-xl font-semibold">{product.name}</h3>
<p className="text-gray-600 mt-2">{product.description}</p>
<p className="text-lg font-bold text-blue-600 mt-3">
${product.price.toFixed(2)}
</p>
</div>
))}
</div>
</div>
);
} catch (error) {
return (
<div className="error-container">
<h1>Lỗi tải dữ liệu</h1>
<p>Không thể tải danh sách sản phẩm</p>
</div>
);
}
}
3.2. Data fetching SSR (cache: “no-store”)
// app/live/page.tsx – SSR với realtime data
interface LiveData {
timestamp: number;
usersOnline: number;
serverStatus: string;
metrics: {
cpu: number;
memory: number;
};
}
export default async function LivePage() {
const res = await fetch("https://api.example.com/live", {
cache: "no-store", // Luôn fetch mới
});
const data: LiveData = await res.json();
return (
<div className="live-container">
<h1>Realtime Data Dashboard</h1>
<div className="last-updated">
Last updated: {new Date(data.timestamp).toLocaleTimeString()}
</div>
<div className="metrics-grid">
<div className="metric-card">
<h3>Users Online</h3>
<p className="metric-value">{data.usersOnline}</p>
</div>
<div className="metric-card">
<h3>Server Status</h3>
<p className={`status ${data.serverStatus.toLowerCase()}`}>
{data.serverStatus}
</p>
</div>
<div className="metric-card">
<h3>CPU Usage</h3>
<p className="metric-value">{data.metrics.cpu}%</p>
</div>
<div className="metric-card">
<h3>Memory Usage</h3>
<p className="metric-value">{data.metrics.memory}%</p>
</div>
</div>
</div>
);
}
3.3. Data fetching với revalidate (ISR)
// app/news/page.tsx – ISR với revalidate 2 phút
export const revalidate = 120; // Giây
interface NewsArticle {
id: string;
title: string;
content: string;
publishedAt: string;
author: string;
}
export default async function NewsPage() {
const res = await fetch("https://api.example.com/news");
const articles: NewsArticle[] = await res.json();
return (
<div className="news-container">
<h1 className="text-3xl font-bold mb-6">Tin tức mới nhất</h1>
<div className="last-updated text-sm text-gray-500 mb-4">
Last updated: {new Date().toLocaleTimeString()}
</div>
<div className="articles-grid">
{articles.map((article) => (
<article key={article.id} className="news-card">
<h2 className="text-xl font-semibold mb-2">{article.title}</h2>
<p className="text-gray-600 mb-3">{article.content}</p>
<div className="article-meta">
<span className="text-sm text-gray-500">
By {article.author}
</span>
<span className="text-sm text-gray-500 ml-4">
{new Date(article.publishedAt).toLocaleDateString()}
</span>
</div>
</article>
))}
</div>
</div>
);
}
3.4. Client Component fetching
// app/weather/page.tsx
"use client";
import { useEffect, useState } from "react";
interface WeatherData {
temperature: number;
condition: string;
humidity: number;
windSpeed: number;
}
export default function WeatherWidget() {
const [weather, setWeather] = useState<WeatherData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch("/api/weather")
.then((res) => {
if (!res.ok) throw new Error("Failed to fetch weather");
return res.json();
})
.then((data) => {
setWeather(data);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <div>Loading weather...</div>;
if (error) return <div>Error: {error}</div>;
if (!weather) return null;
return (
<div className="weather-widget">
<h2>Thời tiết hiện tại</h2>
<div className="weather-main">
<span className="temperature">{weather.temperature}°C</span>
<span className="condition">{weather.condition}</span>
</div>
<div className="weather-details">
<p>Độ ẩm: {weather.humidity}%</p>
<p>Tốc độ gió: {weather.windSpeed} km/h</p>
</div>
</div>
);
}
3.5. Advanced: Parallel Data Fetching với Promise.all
// app/dashboard/page.tsx – Parallel fetching
interface DashboardData {
users: any[];
orders: any[];
stats: any;
}
export default async function DashboardPage() {
// Fetch song song để tối ưu thời gian
const [usersRes, ordersRes, statsRes] = await Promise.all([
fetch('https://api.example.com/users'),
fetch('https://api.example.com/orders'),
fetch('https://api.example.com/stats')
]);
const [users, orders, stats] = await Promise.all([
usersRes.json(),
ordersRes.json(),
statsRes.json()
]);
return (
<div className="dashboard">
<h1>Dashboard</h1>
<div className="dashboard-grid">
<div className="widget">
<h3>Users ({users.length})</h3>
{/* Render users */}
</div>
<div className="widget">
<h3>Orders ({orders.length})</h3>
{/* Render orders */}
</div>
<div className="widget">
<h3>Stats</h3>
{/* Render stats */}
</div>
</div>
</div>
);
}
4. Kiến thức trọng tâm
4.1. Server Components có thể gọi API trực tiếp mà không gửi JavaScript xuống client
Đây là lợi thế lớn của App Router:
- Code fetch chỉ chạy ở server
- Client nhận HTML đã render + minimal JS
- Tốt cho SEO và performance
4.2. Cache options trong Next.js
// force-cache (default) – SSG
fetch('https://...', { cache: 'force-cache' })
// no-store – SSR
fetch('https://...', { cache: 'no-store' })
// revalidate – ISR
fetch('https://...', { next: { revalidate: 60 } })
// Hoặc dùng route-level revalidate
export const revalidate = 60;
4.3. Client Components dùng useEffect để fetch dữ liệu sau khi render, không phù hợp cho SEO
"use client";
import { useEffect, useState } from 'react';
// Không tốt cho SEO vì content render sau
export default function ClientData() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []);
return <div>{data ? data.title : 'Loading...'}</div>;
}
5. Bài tập nhanh
5.1. Tạo 1 trang dùng SSR, 1 trang dùng SSG và 1 trang dùng ISR bằng App Router
// pages/ssr/page.tsx – SSR
export default async function SSRPage() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
});
// ...
}
// pages/ssg/page.tsx – SSG
export default async function SSGPage() {
const res = await fetch('https://api.example.com/data');
// ...
}
// pages/isr/page.tsx – ISR
export const revalidate = 60;
export default async function ISRPage() {
const res = await fetch('https://api.example.com/data');
// ...
}
5.2. Tạo widget client hiển thị thời tiết realtime
"use client";
import { useEffect, useState } from 'react';
export default function WeatherWidget() {
const [weather, setWeather] = useState(null);
useEffect(() => {
// Fetch weather data
fetch('/api/weather')
.then(res => res.json())
.then(setWeather);
// Update mỗi 5 phút
const interval = setInterval(() => {
fetch('/api/weather')
.then(res => res.json())
.then(setWeather);
}, 5 * 60 * 1000);
return () => clearInterval(interval);
}, []);
return (
<div className="weather">
{weather && (
<div>
<p>{weather.temp}°C</p>
<p>{weather.condition}</p>
</div>
)}
</div>
);
}
5.3. Tối ưu một trang bằng cách chuyển phần cố định sang Server Component, phần tương tác sang Client Component
Before (tất cả Client Component):
"use client";
export default function ProductPage() {
const [products, setProducts] = useState([]);
const [filter, setFilter] = useState('');
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(setProducts);
}, []);
return (
<div>
<h1>Sản phẩm</h1> {/* Cố định */}
<input
value={filter}
onChange={e => setFilter(e.target.value)}
/> {/* Tương tác */}
{products.map(p => <div>{p.name}</div>)}
</div>
);
}
After (tối ưu):
// app/products/page.tsx – Server Component
export default async function ProductPage() {
const products = await fetch('https://api.example.com/products')
.then(res => res.json());
return (
<div>
<h1>Sản phẩm</h1>
<ProductFilter products={products} />
</div>
);
}
// app/products/ProductFilter.tsx – Client Component
"use client";
export default function ProductFilter({ products }) {
const [filter, setFilter] = useState('');
const filtered = products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
return (
<div>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
/>
{filtered.map(p => <div key={p.id}>{p.name}</div>)}
</div>
);
}
6. Kết luận
App Router mang lại mô hình data fetching thống nhất, mạnh mẽ và linh hoạt. Việc hiểu rõ cache, render mode và cách tách server/client giúp xây dựng ứng dụng hiệu quả và tối ưu chi phí.
Tóm tắt key points:
- Dùng
fetch()với các cache options để control behavior - Server Components cho SEO và performance
- Client Components cho tương tác và realtime
- Tách biệt concerns để tối ưu nhất
- Luôn cân nhắc giữa performance và functionality needs