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.

In the Pages Router (and most React tutorials) data fetching meant useEffect + useState + a loading flag + an API route to hide your secrets {Ở 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}. The App Router deletes all of that {App Router xóa sạch những thứ đó}. This part shows you how data fetching actually works in Next.js 16 — and how to make it fast {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 in the component, on the server {Fetch ngay trong component, trên server}

Server Components can be async. You await data right where you render it {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>
  );
}

No useEffect, no useState, no loading flag, no API route {Không useEffect, không useState, không cờ loading, không API route}. The fetch runs on the server during render; the browser receives finished HTML {Fetch chạy trên server lúc render; trình duyệt nhận HTML hoàn chỉnh}.

Direct data sources {Nguồn dữ liệu trực tiếp}

Because the code runs on the server, you can skip HTTP entirely and talk to your database or filesystem directly {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} />;
}

Your DB credentials never reach the browser {Thông tin đăng nhập DB không bao giờ tới trình duyệt}. This is the single biggest simplification the App Router brings {Đây là sự đơn giản hóa lớn nhất mà App Router mang lại}.


2. Dynamic by default (the Next.js 16 model) {Động theo mặc định (mô hình Next.js 16)}

In Next.js 16 a plain fetch is not cached — it runs on every request, like any backend {Ở 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');

This is a deliberate reset from Next.js 13–15, where fetches were cached by default and confused everyone {Đâ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}. The new rule is simple {Quy tắc mới đơn giản}: dynamic unless you opt into caching with 'use cache' (Part 4) {động trừ khi bạn chọn cache bằng 'use cache' (Phần 4)}.

Anything that reads the request also forces dynamic rendering {Bất cứ thứ gì đọc request cũng buộc render động}: cookies(), headers(), searchParams, and params {cookies(), headers(), searchParams, và params}. We use those in Parts 7 and 9 {Ta dùng chúng ở Phần 7 và 9}.


3. The waterfall trap {Bẫy thác nước}

The most common performance mistake is sequential awaits that don’t depend on each other {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} />;
}

If two requests are independent, fire them in parallel and await together {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} />;
}

Rule of thumb {Nguyên tắc}: await sequentially only when a later request needs the result of an earlier one {await tuần tự chỉ khi request sau cần kết quả của request trước}. Otherwise, Promise.all {Ngược lại, Promise.all}.


4. Streaming with Suspense {Streaming với Suspense}

You don’t have to make the whole page wait for the slowest query {Bạn không cần bắt cả trang chờ truy vấn chậm nhất}. Wrap a slow component in <Suspense> and Next.js streams the rest of the page immediately, filling the slow hole when it’s ready {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} />;
}

The key insight {Điểm mấu chốt}: the async work moves into the component inside <Suspense>, not the parent {phần việc async chuyển vào trong component bên trong <Suspense>, không phải ở cha}. The parent stays synchronous and renders instantly {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

This is the foundation of Partial Prerendering (PPR), which we complete in Part 4 {Đây là nền tảng của Partial Prerendering (PPR), ta hoàn thiện ở Phần 4}.

Parallel streaming {Streaming song song}

Multiple Suspense boundaries stream independently — each fills in as soon as its own data is ready {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. Passing data to Client Components {Truyền dữ liệu sang Client Components}

A Server Component can render a Client Component and pass it data as props {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
}

The catch {Lưu ý}: props crossing the server→client boundary must be serializable {props đi qua ranh giới server→client phải serialize được}. You can pass objects, arrays, strings, numbers, dates, even Promises — but not functions, class instances, or things like a DB connection {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}.

The “children” composition pattern {Mẫu “children”}

You can’t import a Server Component into a Client Component, but you can pass one as 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>
  );
}

This pattern keeps server-rendered content out of the client bundle while still nesting it inside interactive client UI {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}. Memorize it — it’s how seniors keep bundles small {Hãy nhớ — đây là cách senior giữ bundle nhỏ}.


6. Request memoization {Ghi nhớ request}

If several components in one render need the same data, you don’t need to thread it through props {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 memoizes identical fetch calls within a single render pass, so calling getUser() in three places hits the network once {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());

For non-fetch data sources (e.g. a DB call), wrap the function in React’s cache() to get the same per-request dedup {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 } });
});

Don’t confuse React’s cache() (per-request dedup, in-memory, no persistence) with Next.js 'use cache' (persistent caching across requests, Part 4) {Đừ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. Error handling {Xử lý lỗi}

Throwing inside a Server Component bubbles to the nearest error.tsx (Part 2) {Ném lỗi trong một Server Component nổi lên error.tsx gần nhất (Phần 2)}. For expected “not found” cases, call notFound() instead of throwing {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>;
}

Keep try/catch for cases you can recover from (e.g. show a fallback widget); let unrecoverable errors throw to the boundary {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 — guard your secrets {server-only — bảo vệ secret}

To guarantee a module never accidentally ends up in a client bundle, import the server-only package at the top {Để đả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());
}

If any Client Component imports this module, the build fails with a clear error — a much better outcome than leaking a token {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}. There’s a matching client-only for browser-only code {Có client-only tương ứng cho code chỉ chạy ở trình duyệt}.


9. Exercises {Bài tập}

  1. Async page {Page bất đồng bộ}: build app/users/page.tsx that awaits https://jsonplaceholder.typicode.com/users and lists names — no useEffect {dựng app/users/page.tsx await API và liệt kê tên — không useEffect}.

  2. Fix a waterfall {Sửa thác nước}: fetch a user and their posts sequentially, measure the time, then convert to Promise.all and measure again {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 a slow widget {Stream widget chậm}: add a <Revenue /> component with a 2s artificial delay, wrap it in <Suspense>, and confirm the rest of the page renders instantly {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. Children composition {Mẫu children}: build a 'use client' <Collapsible> and pass a Server Component as its children. Verify in the Network tab that the server content’s data fetch didn’t ship to the client {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. Dedup {Khử trùng lặp}: call the same getUser() in both a layout and its page, add a console.log inside, and confirm it logs once per request {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}.


What’s next {Phần tiếp theo}

You can fetch data without ceremony, avoid waterfalls, stream slow parts, and keep secrets on the server {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}.

Part 4 is the headline of Next.js 16: Cache Components — the 'use cache' directive, cacheLife, cacheTag, and how PPR combines a cached static shell with streamed dynamic holes for instant pages {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ì}.