Pada arsitektur Next.js modern, data fetching tidak lagi identik dengan pola lama seperti getServerSideProps atau getStaticProps. Dengan App Router dan React Server Components, proses pengambilan data berpindah lebih dekat ke tempat data itu benar-benar dibutuhkan: di sisi server, langsung dari komponen atau modul server. Pendekatan ini membuat aplikasi lebih efisien, mengurangi JavaScript yang dikirim ke browser, dan membuka peluang optimasi seperti request deduplication, cache berbasis render, serta revalidation yang lebih presisi.
Artikel ini membahas bagaimana Next.js 16 memanfaatkan pendekatan server-first untuk data fetching, bagaimana menggunakan cache() dari React untuk mencegah request duplikat, dan kapan memakai next: { revalidate } agar halaman tetap cepat tanpa kehilangan kesegaran data. Fokusnya adalah praktik terbaik untuk aplikasi production, bukan sekadar contoh minimal.
Mengapa Data Fetching di Next.js Modern Berubah?
Pada model lama berbasis Pages Router, kita biasanya memilih salah satu dari dua mekanisme utama:
getServerSidePropsuntuk mengambil data setiap request.getStaticPropsuntuk membuat halaman statis saat build, sering dipadukan dengan ISR.
Pendekatan ini bekerja baik, tetapi memiliki keterbatasan. Data fetching menjadi terpusat di level halaman, bukan dekat dengan komponen yang menggunakannya. Akibatnya, komposisi antarkomponen menjadi kurang fleksibel, dan sering kali data harus dioper-oper melalui props dalam jumlah besar.
Di App Router, Server Components memungkinkan komponen melakukan await langsung terhadap sumber data di server. Ini memberi beberapa keuntungan:
- Data fetching lebih dekat ke UI yang membutuhkan data.
- Lebih sedikit JavaScript di client, karena komponen server tidak ikut di-bundle ke browser.
- Optimasi caching lebih natural, baik pada level
fetchmaupun fungsi utilitas yang dibungkuscache(). - Streaming dan rendering bertahap lebih mudah dilakukan.
Dengan kata lain, model baru ini bukan sekadar pengganti API lama, tetapi perubahan arsitektur yang membuat aplikasi lebih modular dan efisien.
Perbedaan dengan getServerSideProps dan getStaticProps
1. Lokasi data fetching
Pada Pages Router, data biasanya diambil di file halaman melalui fungsi khusus. Pada App Router, data bisa diambil langsung di Server Component, misalnya di app/products/page.tsx atau komponen server lain yang diimpor halaman tersebut.
2. Granularitas caching
Pada pola lama, strategi rendering cenderung ditetapkan di level halaman: SSR, SSG, atau ISR. Pada App Router, caching bisa lebih granular. Satu sumber data bisa memiliki revalidate tertentu, sementara sumber lain bisa selalu segar atau sepenuhnya statis.
3. Komposisi komponen
Server Components membuat komponen lebih mudah dipecah. Komponen daftar produk dapat mengambil data sendiri, komponen sidebar dapat mengambil data lain, dan keduanya tidak harus dikumpulkan dulu di root page.
4. Pengurangan beban client
Karena banyak logika berjalan di server, browser menerima hasil render yang lebih ringan. Ini penting untuk performa, terutama pada perangkat low-end atau koneksi lambat.
Catatan: Jika Anda masih berpikir dalam pola “semua data harus diambil di level halaman”, Anda akan kehilangan banyak manfaat App Router. Komposisi berbasis server adalah inti dari pendekatan baru ini.
Struktur Project Next.js 16 dengan App Router
Berikut contoh struktur folder yang umum untuk aplikasi production sederhana:
my-next-app/
├─ app/
│ ├─ products/
│ │ ├─ page.tsx
│ │ └─ loading.tsx
│ ├─ layout.tsx
│ └─ page.tsx
├─ components/
│ ├─ product-list.tsx
│ └─ product-card.tsx
├─ lib/
│ ├─ api.ts
│ └─ products.ts
├─ types/
│ └─ product.ts
├─ package.json
├─ tsconfig.json
└─ next.config.tsPemisahan seperti ini membantu menjaga tanggung jawab kode tetap jelas:
app/untuk route dan komposisi UI.components/untuk komponen presentasional atau komponen server reusable.lib/untuk akses data, utilitas API, dan fungsi cache.types/untuk definisi tipe TypeScript.
Contoh Data Fetching di Server Component
Kita mulai dari definisi tipe sederhana:
// types/product.ts
export type Product = {
id: number;
title: string;
price: number;
category: string;
};Lalu buat fungsi pengambilan data dari API di lib/api.ts:
// lib/api.ts
import { Product } from '@/types/product';
export async function fetchProducts(): Promise<Product[]> {
const response = await fetch('https://dummyjson.com/products', {
next: { revalidate: 300 },
});
if (!response.ok) {
throw new Error('Gagal mengambil data produk');
}
const json = await response.json();
return json.products;
}Di atas, next: { revalidate: 300 } berarti Next.js boleh menyajikan hasil cache dan memperbaruinya kembali setiap 300 detik. Ini adalah kompromi yang baik untuk data yang tidak berubah setiap detik.
Kemudian gunakan langsung di Server Component:
// app/products/page.tsx
import { fetchProducts } from '@/lib/api';
export default async function ProductsPage() {
const products = await fetchProducts();
return (
<main>
<h1>Daftar Produk</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
{product.title} - Rp{product.price}
</li>
))}
</ul>
</main>
);
}Karena ini adalah Server Component, kita bisa menggunakan await langsung tanpa useEffect, tanpa state loading di client, dan tanpa perlu API route tambahan jika memang tidak dibutuhkan.
Menggunakan React cache() untuk Mencegah Duplicate Request
Salah satu masalah yang sering muncul pada aplikasi modular adalah beberapa komponen memerlukan data yang sama. Misalnya, halaman produk mengambil daftar produk, lalu komponen sidebar atau metadata juga memerlukan data serupa. Jika tidak hati-hati, ini bisa menyebabkan request berulang ke endpoint yang sama.
Untuk kasus seperti ini, React menyediakan cache(). Fungsi ini berguna untuk memoization pada server, sehingga pemanggilan fungsi async yang sama dengan argumen sama dapat memakai hasil yang sudah dihitung selama siklus render yang relevan.
// lib/products.ts
import { cache } from 'react';
import { Product } from '@/types/product';
export const getProducts = cache(async (): Promise<Product[]> => {
const response = await fetch('https://dummyjson.com/products', {
next: { revalidate: 300 },
});
if (!response.ok) {
throw new Error('Gagal mengambil data produk');
}
const json = await response.json();
return json.products;
});
export const getProductById = cache(async (id: string): Promise<Product> => {
const response = await fetch(`https://dummyjson.com/products/${id}`, {
next: { revalidate: 300 },
});
if (!response.ok) {
throw new Error(`Gagal mengambil produk dengan id ${id}`);
}
return response.json();
});Kini, jika getProducts() dipanggil dari beberapa tempat pada render tree yang sama, React dapat mencegah eksekusi berulang yang tidak perlu.
Contoh penggunaannya:
// components/product-list.tsx
import { getProducts } from '@/lib/products';
export default async function ProductList() {
const products = await getProducts();
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
);
}// app/products/page.tsx
import ProductList from '@/components/product-list';
import { getProducts } from '@/lib/products';
export default async function ProductsPage() {
const products = await getProducts();
return (
<main>
<h1>Total produk: {products.length}</h1>
<ProductList />
</main>
);
}Pada contoh di atas, getProducts() dipanggil di dua tempat. Dengan cache(), Anda mengurangi risiko duplicate request yang sebetulnya meminta data identik.
Kapan cache() paling berguna?
- Saat beberapa komponen server memakai sumber data sama.
- Saat fungsi pengambilan data melibatkan transformasi mahal.
- Saat Anda ingin membuat lapisan akses data reusable di
lib/.
Hal yang perlu dipahami
cache() bukan pengganti semua strategi caching Next.js. Ia lebih tepat dipakai untuk memoization fungsi pada sisi server. Jika Anda ingin mengontrol kesegaran data lintas request, tetap pertimbangkan fetch caching, revalidate, atau strategi invalidasi yang sesuai.
Memahami next: { revalidate }
Pada App Router, salah satu cara paling praktis untuk mengontrol keseimbangan antara performa dan kesegaran data adalah opsi next: { revalidate } pada fetch.
const response = await fetch('https://api.example.com/articles', {
next: { revalidate: 600 },
});Artinya, respons dapat di-cache dan diregenerasi maksimal setiap 600 detik. Ini cocok untuk:
- Daftar produk yang berubah berkala.
- Halaman konten editorial.
- Data dashboard yang tidak perlu real-time absolut.
Sebaliknya, untuk data yang benar-benar harus segar setiap request, Anda bisa mempertimbangkan pola tanpa cache atau konfigurasi yang lebih sesuai dengan kebutuhan endpoint tersebut. Yang terpenting, jangan menggunakan data selalu-dinamis untuk semua route jika sebenarnya tidak perlu, karena itu akan membebani server dan memperlambat respons.
Praktik terbaik memilih nilai revalidate
- 30-60 detik untuk data semi-real-time.
- 300-900 detik untuk katalog, artikel, atau halaman listing yang relatif stabil.
- Lebih lama untuk data referensi yang jarang berubah.
Nilai ideal bergantung pada toleransi bisnis terhadap data usang. Jangan memilih angka kecil hanya karena terdengar “lebih real-time”, jika dampaknya adalah peningkatan beban sistem tanpa manfaat nyata.
Pola Production-Ready untuk Data Layer
1. Simpan akses data di lib/
Hindari menaruh seluruh logika fetch langsung di file halaman. Buat lapisan akses data seperti lib/products.ts agar mudah diuji, di-cache, dan digunakan ulang.
2. Validasi response dan tangani error
Jangan mengasumsikan API selalu mengembalikan struktur yang benar. Minimal periksa response.ok. Untuk sistem production, pertimbangkan validasi skema dengan library seperti Zod bila struktur data eksternal sering berubah.
3. Pisahkan komponen server dan client dengan sadar
Jangan menambahkan 'use client' tanpa alasan kuat. Semakin banyak komponen tetap berada di server, semakin kecil bundle JavaScript di browser.
4. Hindari request ganda yang tidak disadari
Request ganda sering terjadi karena:
- Fungsi fetch dipanggil di beberapa komponen tanpa memoization.
- Data yang sama diambil ulang di route, layout, dan komponen anak.
- Developer mencampur fetch server dengan fetch client untuk resource yang sama.
Gunakan cache() untuk fungsi utilitas data yang sering dipakai ulang, dan audit jalur pemanggilan data saat halaman mulai kompleks.
5. Gunakan loading UI secara strategis
App Router mendukung file loading.tsx. Ini berguna untuk menjaga pengalaman pengguna saat bagian halaman sedang di-stream.
// app/products/loading.tsx
export default function LoadingProducts() {
return <p>Memuat daftar produk...</p>;
}Debugging dan Kesalahan Umum
1. Mengira semua fetch otomatis bebas duplikasi
Walaupun Next.js memiliki optimasi caching pada fetch, tetap ada kasus di mana Anda lebih aman membungkus fungsi akses data dengan cache(), terutama jika fungsi tersebut dipanggil dari banyak tempat atau memiliki logika tambahan.
2. Terlalu banyak komponen client
Jika halaman terasa berat di browser, cek apakah komponen yang seharusnya server justru diberi 'use client'. Ini adalah penyebab umum bundle membengkak.
3. Revalidate terlalu agresif atau terlalu longgar
Jika data sering basi, mungkin nilai revalidate terlalu besar. Jika server terlalu sering bekerja ulang, mungkin nilainya terlalu kecil. Monitor pola perubahan data bisnis, bukan hanya preferensi teknis.
4. Tidak memusatkan akses API
Jika endpoint tersebar di banyak file, debugging menjadi sulit. Pusatkan endpoint, header, autentikasi, dan penanganan error di lapisan lib/.
Contoh Menjalankan Project
npx create-next-app@latest my-next-app
cd my-next-app
npm install
npm run devSetelah itu, tambahkan file dan struktur seperti contoh sebelumnya. Pastikan alias path TypeScript seperti @/lib dan @/types sudah sesuai dengan konfigurasi project Anda.
Penutup
Strategi data fetching di Next.js 16 berfokus pada arsitektur server-first: ambil data di Server Components, letakkan logika akses data di lapisan yang reusable, gunakan cache() untuk mencegah request berulang, dan terapkan next: { revalidate } untuk menyeimbangkan performa dengan kesegaran data.
Jika Anda datang dari pola getServerSideProps dan getStaticProps, perubahan ini mungkin terasa signifikan. Namun, setelah dipahami, model baru ini memberi kontrol yang lebih baik terhadap komposisi UI, efisiensi jaringan, dan performa production. Kunci utamanya adalah memahami bahwa caching bukan fitur tambahan, melainkan bagian inti dari desain aplikasi modern di Next.js.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!