Episode ini mengajarkan pengambilan data bertahap: useInfiniteQuery dengan getNextPageParam untuk tombol Load More, dan pagination sederhana berbasis nomor halaman dengan placeholderData dan keepPreviousData agar UI tetap halus.

Daftar todo ratusan item, feed berita yang terus memanjang, atau komentar ribuan — data seperti ini tidak mungkin diambil sekaligus. Dua pendekatan umum: pagination dengan nomor halaman, dan infinite scroll / Load More yang memuat halaman berikutnya saat user membutuhkannya.
Episode 9 membahas keduanya: useInfiniteQuery untuk pola Load More, dan pagination sederhana dengan keepPreviousData agar perpindahan halaman tidak menampilkan layar loading yang berkedip.
Pendekatan paling lurus: simpan nomor halaman di useState, lalu jadikan bagian dari queryKey:
const [page, setPage] = useState(1)
const { data, isPending } = useQuery({
queryKey: ["todos", "page", page],
queryFn: () =>
fetch(`https://jsonplaceholder.typicode.com/todos?_page=${page}&_limit=10`).then(
(res) => res.json()
),
})
return (
<div>
{data?.map((todo) => <p key={todo.id}>{todo.title}</p>)}
<button onClick={() => setPage((p) => Math.max(1, p - 1))}>Sebelumnya</button>
<button onClick={() => setPage((p) => p + 1)}>Berikutnya</button>
</div>
)Setiap page punya cache sendiri karena page ada di dalam queryKey. Saat pindah halaman, TanStack Query menampilkan loading penuh — di sinilah keepPreviousData datang menyelamatkan.
Untuk mencegah flicker saat pindah halaman, pertahankan data halaman sebelumnya sebagai placeholder:
import { keepPreviousData } from "@tanstack/react-query"
const { data, isFetching } = useQuery({
queryKey: ["todos", "page", page],
queryFn: () =>
fetch(`https://jsonplaceholder.typicode.com/todos?_page=${page}&_limit=10`).then(
(res) => res.json()
),
placeholderData: keepPreviousData,
})placeholderData: keepPreviousData membuat data halaman sebelumnya tetap tampil sementara halaman baru diambil. isFetching bernilai true selama proses itu — bisa dipakai untuk menampilkan indikator halus tanpa mengganti konten. Hasilnya navigasi halaman terasa instan.
Untuk pola Load More, gunakan useInfiniteQuery. Perbedaannya dengan useQuery: data dikumpulkan dalam bentuk pages (array halaman) plus pageParams, dan kalian memberi tahu library cara menentukan halaman berikutnya lewat getNextPageParam:
import { useInfiniteQuery } from "@tanstack/react-query"
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
queryKey: ["todos", "infinite"],
queryFn: ({ pageParam }) =>
fetch(
`https://jsonplaceholder.typicode.com/todos?_page=${pageParam}&_limit=10`
).then((res) => res.json()),
initialPageParam: 1,
getNextPageParam: (lastPage, _pages, lastPageParam) =>
lastPage.length === 10 ? lastPageParam + 1 : undefined,
})getNextPageParam menerima halaman terakhir dan mengembalikan halaman berikutnya — atau undefined untuk menandai tidak ada halaman lagi. initialPageParam: 1 menetapkan halaman pertama. useInfiniteQuery memanggil queryFn dengan pageParam yang dihitung otomatis.
Menampilkan semua halaman dan tombol untuk memuat berikutnya:
function Todos() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
queryKey: ["todos", "infinite"],
queryFn: ({ pageParam }) =>
fetch(
`https://jsonplaceholder.typicode.com/todos?_page=${pageParam}&_limit=10`
).then((res) => res.json()),
initialPageParam: 1,
getNextPageParam: (lastPage, _pages, lastPageParam) =>
lastPage.length === 10 ? lastPageParam + 1 : undefined,
})
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.map((todo) => (
<p key={todo.id}>{todo.title}</p>
))}
</div>
))}
<button onClick={() => fetchNextPage()} disabled={!hasNextPage}>
{isFetchingNextPage ? "Memuat..." : hasNextPage ? "Load More" : "Tidak ada lagi"}
</button>
</div>
)
}data.pages adalah array berisi seluruh halaman yang sudah dimuat. fetchNextPage() mengambil halaman berikutnya, hasNextPage bernilai false ketika getNextPageParam mengembalikan undefined, dan isFetchingNextPage menandakan proses sedang berjalan.
Jika pola navigasi kalian dua arah — chat atau feed yang bisa scroll ke atas — getPreviousPageParam memberikan arah sebaliknya:
getPreviousPageParam: (firstPage, _pages, firstPageParam) =>
firstPageParam > 1 ? firstPageParam - 1 : undefined,getPreviousPageParam bekerja seperti versi mundur dari getNextPageParam, dengan fetchPreviousPage sebagai pasangan method-nya. getNextPageParam dan getPreviousPageParam boleh dipakai bersamaan untuk infinite query dua arah.
Tip
Untuk infinite scroll otomatis (tanpa tombol), gabungkan fetchNextPage dengan IntersectionObserver pada elemen sentinel di bawah list. TanStack Query tidak menyediakan infinite scroll scroll-event — itu tanggung jawab kalian — tapi method yang dibutuhkan sudah lengkap.
Episode 9 memberi kalian dua pola pengambilan data bertahap yang umum di aplikasi nyata: pagination bernomor halaman dengan keepPreviousData untuk UX halus, dan infinite query dengan tombol Load More yang dikendalikan getNextPageParam.
Inti yang harus dibawa pulang:
queryKey dan queryFn.placeholderData: keepPreviousData mencegah flicker saat pindah halaman.useInfiniteQuery mengumpulkan data dalam bentuk pages.getNextPageParam menentukan halaman berikutnya atau undefined.fetchNextPage dan hasNextPage mengendalikan tombol Load More.getPreviousPageParam untuk navigasi dua arah.Di episode 10 selanjutnya kita akan membahas prefetching dan data seeding — prefetchQuery untuk menanankan data sebelum render, plus initialData dan placeholderData untuk render instan. Ini teknik penting untuk UX yang terasa super responsif.