chúng ta đã làm quen với cấu trúc thư mục và App Router của Next.js. Hôm nay đi vào phần thực tế hơn: lấy dữ liệu từ server, và xử lý các trạng thái đi kèm — loading, lỗi, và trang không tồn tại. Fetch dữ liệu trong Server Component
Next.js mặc định mọi component đều là Server Component — tức là chúng chạy trên server, không phải trên trình duyệt. Nhờ đó, bạn có thể kết nối thẳng vào database và query dữ liệu ngay bên trong component, không cần tạo API route trung gian.
Sau đó dùng ngay trong component, chỉ cần thêm async:
Lưu ý: Nếu bạn cần lấy dữ liệu từ API bên ngoài (third-party), vẫn có thể dùng fetch() trong Server Component — không cần useEffect.
Chỉ dùng useEffect khi component đó là Client Component ('use client').
Vậy khi nào thực sự bắt buộc phải dùng useEffect trong Client Component?
1. Cần tương tác với browser API
Những thứ chỉ tồn tại trong trình duyệt — localStorage, sessionStorage, navigator, window, document. Server không có những thứ này, nên không thể chạy ở Server Component.
2. Fetch dữ liệu phụ thuộc vào hành động của người dùng (sau khi trang đã load)
Server Component chỉ chạy một lần khi render. Nếu bạn cần re-fetch dựa trên input của người dùng — tìm kiếm live-search, infinite scroll, autocomplete — thì phải ở client.
3. Subscribe vào real-time data
WebSocket, SSE (Server-Sent Events), hoặc các subscription như Firebase/Supabase realtime — những thứ cần kết nối liên tục trong suốt vòng đời component.
4. Tích hợp thư viện bên thứ ba cần DOM
Nhiều thư viện JS (chart, map, rich text editor…) cần DOM node thực sự mới khởi tạo được — không thể chạy trên server.
Tóm lại là…
Ranh giới rõ ràng nhất là: Server Component fetch là one-shot khi render — còn useEffect là để phản ứng với những thứ xảy ra sau đó trên trình duyệt.
Hiển thị loading với loading.tsx
Next.js có một file reserved là loading.tsx. Đặt nó cùng thư mục với page.tsx, Next sẽ tự động hiển thị component này trong khi trang đang chờ dữ liệu.
Cách này đơn giản, nhưng có một hạn chế: nó ẩn toàn bộ trang trong lúc loading — kể cả những phần tĩnh không cần chờ dữ liệu.
Dùng <Suspense> để loading đúng chỗ
Giải pháp tốt hơn là tách phần cần dữ liệu thành một component riêng, rồi bọc nó bằng <Suspense>:
Như vậy, tiêu đề <h1> hiển thị ngay lập tức, chỉ có danh sách bên dưới mới chờ — đúng behavior mà người dùng mong đợi.
Xử lý lỗi với error.tsx
Tương tự, Next.js có file reserved error.tsx để bắt lỗi xảy ra trong quá trình render:
Có hai điểm quan trọng:
Phải thêm 'use client' ở đầu file — đây là yêu cầu bắt buộc của Next.js vì error boundary hoạt động ở phía client. Component nhận vào prop error (để hiển thị thông điệp lỗi) và reset (để thử render lại). Xử lý 404 với not-found.tsx
Khi một trang hoặc resource không tồn tại, dùng file not-found.tsx để tạo template 404:
Sau đó, dùng notFound() khi route hợp lệ nhưng data không tồn tại — thường gặp nhất ở dynamic routes, import từ next/navigation:
File not-found.tsx có thể đặt ở nhiều cấp khác nhau:
Next.js sẽ dùng file not-found.tsx gần nhất với nơi gọi notFound().
Lưu ý thực tế (production)
loading.tsx vs <Suspense>
Dùng loading.tsx cho các trang có toàn bộ nội dung phụ thuộc vào data (trang dashboard, trang danh sách). Dùng <Suspense> khi trang có cả phần tĩnh lẫn phần động — đây là pattern được khuyến nghị nhiều hơn vì UX tốt hơn.
error.tsx nên có nút “Thử lại”
Prop reset cho phép re-render lại segment hiện tại mà không cần reload toàn trang. Đây là chi tiết nhỏ nhưng cải thiện UX đáng kể, đừng bỏ qua.
Không log thông tin nhạy cảm ra error.message
Ở production, error.message đôi khi có thể chứa thông tin nội bộ (tên bảng, connection string…). Nên log đầy đủ ở server (dùng console.error hoặc dịch vụ như Sentry), còn hiển thị ra UI chỉ nên là thông điệp chung chung.
Scope not-found.tsx cẩn thận
Đặt not-found.tsx ở app/ sẽ override trang 404 mặc định của Next — hãy đảm bảo nó đủ thông tin, có link về trang chủ, và không quá “trống”.
Tóm tắt