jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Next.js 16 from Zero to Senior · Part 3 — Server Components & Data Fetching

Fetch data the App Router way: async Server Components, avoiding request waterfalls with parallel fetching, streaming slow parts with Suspense, the server/client data handoff, and the dynamic-by-default model of Next.js 16.

Ở Pages Router (và phần lớn tutorial React) fetch dữ liệu nghĩa là useEffect + useState + cờ loading + một API route để giấu secret. App Router xóa sạch những thứ đó. Phần này chỉ cho bạn cách fetch dữ liệu thực sự hoạt động ở Next.js 16 — và cách làm nó nhanh.


1. Fetch ngay trong component, trên server

Server Component có thể async. Bạn await dữ liệu ngay nơi bạn render:

// app/users/page.tsx  (Server Component)
async function getUsers() {
  const res = await fetch('https://api.example.com/users');
  if (!res.ok) throw new Error('Failed to load users');
  return res.json() as Promise<User[]>;
}

export default async function UsersPage() {
  const users = await getUsers();
  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

Không useEffect, không useState, không cờ loading, không API route. Fetch chạy trên server lúc render; trình duyệt nhận HTML hoàn chỉnh.

Nguồn dữ liệu trực tiếp

Vì code chạy trên server, bạn có thể bỏ qua HTTP hoàn toàn và nói chuyện trực tiếp với database hay filesystem:

import { db } from '@/lib/db';

export default async function UsersPage() {
  const users = await db.user.findMany(); // runs on the server only
  return <UserList users={users} />;
}

Thông tin đăng nhập DB không bao giờ tới trình duyệt. Đây là sự đơn giản hóa lớn nhất mà App Router mang lại.


2. Động theo mặc định (mô hình Next.js 16)

Ở Next.js 16 một fetch thường không được cache — nó chạy mỗi request, như bất kỳ backend nào:

// Fresh on every request. No stale data surprises.
const res = await fetch('https://api.example.com/price');

Đây là sự thiết lập lại có chủ ý so với Next.js 13–15, nơi fetch được cache mặc định và làm mọi người bối rối. Quy tắc mới đơn giản: động trừ khi bạn chọn cache bằng 'use cache' (Phần 4).

Bất cứ thứ gì đọc request cũng buộc render động: cookies(), headers(), searchParams, cookies(), headers(), searchParams, và params. Ta dùng chúng ở Phần 7 và 9.


3. Bẫy thác nước

Lỗi hiệu năng phổ biến nhất là await tuần tự mà không phụ thuộc nhau:

// ❌ Waterfall: user finishes, THEN posts start. Total = sum of both.
export default async function Profile() {
  const user = await getUser();    // 300ms
  const posts = await getPosts();  // 300ms  → 600ms total
  return <Page user={user} posts={posts} />;
}

Nếu hai request độc lập, bắn chúng song song và await cùng lúc:

// ✅ Parallel: both start immediately. Total = max of both.
export default async function Profile() {
  const [user, posts] = await Promise.all([getUser(), getPosts()]); // ~300ms
  return <Page user={user} posts={posts} />;
}

Nguyên tắc: await tuần tự chỉ khi request sau cần kết quả của request trước. Ngược lại, Promise.all.


4. Streaming với Suspense

Bạn không cần bắt cả trang chờ truy vấn chậm nhất. Bọc một component chậm trong <Suspense> và Next.js stream phần còn lại của trang ngay, lấp chỗ chậm khi nó sẵn sàng:

import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <>
      <Header />               {/* fast — renders immediately */}
      <Suspense fallback={<RevenueSkeleton />}>
        <Revenue />            {/* slow — streams in when ready */}
      </Suspense>
    </>
  );
}

async function Revenue() {
  const data = await getRevenue(); // 2s query
  return <Chart data={data} />;
}

Điểm mấu chốt: phần việc async chuyển vào trong component bên trong <Suspense>, không phải ở cha. Cha vẫn đồng bộ và render tức thì.

Time →
0ms    [Header][skeleton............]   ← user sees layout immediately
2000ms [Header][Revenue chart filled]   ← slow part streamed in

Đây là nền tảng của Partial Prerendering (PPR), ta hoàn thiện ở Phần 4.

Streaming song song

Nhiều ranh giới Suspense stream độc lập — mỗi cái lấp đầy ngay khi dữ liệu của riêng nó sẵn sàng:

<Suspense fallback={<Sk />}><Revenue /></Suspense>
<Suspense fallback={<Sk />}><Orders /></Suspense>
<Suspense fallback={<Sk />}><Activity /></Suspense>

5. Truyền dữ liệu sang Client Components

Một Server Component có thể render một Client Component và truyền dữ liệu cho nó qua props:

// app/page.tsx  (Server Component)
import { Chart } from './chart'; // a 'use client' component

export default async function Page() {
  const data = await getChartData(); // fetched on the server
  return <Chart data={data} />;      // passed to the client as props
}

Lưu ý: props đi qua ranh giới server→client phải serialize được. Bạn có thể truyền object, array, string, number, date, kể cả Promise — nhưng không truyền function, instance của class, hay thứ như kết nối DB.

Mẫu “children”

Bạn không thể import một Server Component vào một Client Component, nhưng bạn có thể truyền nó làm children:

// A client wrapper (e.g. an animated panel)
'use client';
export function Panel({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(true);
  return open ? <div>{children}</div> : null;
}

// Server Component composes them — ServerData stays on the server
export default function Page() {
  return (
    <Panel>
      <ServerData /> {/* still a Server Component, rendered on the server */}
    </Panel>
  );
}

Mẫu này giữ nội dung render-server ngoài bundle client mà vẫn lồng nó trong UI client tương tác. Hãy nhớ — đây là cách senior giữ bundle nhỏ.


6. Ghi nhớ request

Nếu nhiều component trong một lần render cần cùng dữ liệu, bạn không cần luồn nó qua props. React ghi nhớ các lời gọi fetch giống hệt trong một lần render, nên gọi getUser() ở ba nơi chỉ chạm mạng một lần:

// Called in layout AND page — fetched only once per request.
const user = await fetch('https://api.example.com/me').then((r) => r.json());

Với nguồn dữ liệu không phải fetch (vd gọi DB), bọc hàm trong cache() của React để có cùng cơ chế dedup theo request:

import { cache } from 'react';
import { db } from '@/lib/db';

export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } });
});

Đừng nhầm cache() của React (dedup theo request, trong bộ nhớ, không lưu lâu) với 'use cache' của Next.js (cache lâu dài qua nhiều request, Phần 4).


7. Xử lý lỗi

Ném lỗi trong một Server Component nổi lên error.tsx gần nhất (Phần 2). Với trường hợp “không tìm thấy” dự kiến, gọi notFound() thay vì ném:

import { notFound } from 'next/navigation';

export default async function Post({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const post = await getPost(id);
  if (!post) notFound();             // → renders not-found.tsx, sends 404
  return <article>{post.body}</article>;
}

Giữ try/catch cho trường hợp có thể phục hồi (vd hiện widget dự phòng); để lỗi không phục hồi được ném lên boundary.


8. server-only — bảo vệ secret

Để đảm bảo một module không bao giờ vô tình lọt vào bundle client, import package server-only ở đầu:

import 'server-only';

export async function getSecretData() {
  return fetch('https://internal', {
    headers: { Authorization: `Bearer ${process.env.SECRET}` },
  }).then((r) => r.json());
}

Nếu bất kỳ Client Component nào import module này, build sẽ thất bại với lỗi rõ ràng — kết quả tốt hơn nhiều so với rò rỉ token. Có client-only tương ứng cho code chỉ chạy ở trình duyệt.


9. Bài tập

  1. Page bất đồng bộ: dựng app/users/page.tsx await API và liệt kê tên — không useEffect.

  2. Sửa thác nước: fetch user và bài viết của họ tuần tự, đo thời gian, rồi đổi sang Promise.all và đo lại.

  3. Stream widget chậm: thêm <Revenue /> có delay giả 2s, bọc trong <Suspense>, xác nhận phần còn lại render tức thì.

  4. Mẫu children: dựng <Collapsible> 'use client' và truyền một Server Component làm children. Kiểm tra ở tab Network rằng fetch của nội dung server không gửi về client.

  5. Khử trùng lặp: gọi cùng getUser() ở cả layout và page, thêm console.log bên trong, xác nhận nó log một lần mỗi request.


Phần tiếp theo

Bạn fetch dữ liệu không rườm rà, tránh thác nước, stream phần chậm, và giữ secret trên server.

Phần 4 là điểm nhấn của Next.js 16: Cache Components — directive 'use cache', cacheLife, cacheTag, và cách PPR kết hợp vỏ tĩnh đã cache với các “lỗ” động được stream để có trang tức thì.