Episode ini mendalami fitur lanjutan RTK Query: pagination dan infinite query, GraphQL dengan graphql-request, streaming updates, prefetch berprioritas, codegen dari OpenAPI, serta custom baseQuery untuk multi-API dan otomatis auth refresh saat token kedaluwarsa.

Episode 8 dan 9 memberi fondasi RTK Query: query, mutation, dan invalidation. Aplikasi nyata menuntut lebih: daftar tak berujung, data real-time, GraphQL, dan API client yang diturunkan dari spesifikasi. Episode 17 membahas fitur lanjutan tersebut satu per satu.
Kita akan memakai pagination dan infinite query, berkomunikasi dengan server GraphQL, menerima streaming updates, mengatur prioritas prefetch, meng-generate endpoint dari OpenAPI, serta menulis custom baseQuery yang menangani refresh token secara otomatis.
Cara paling umum: simpan nomor halaman di state lokal, dan lewatkan sebagai argumen query. Cache RTK Query akan memisahkan data per halaman secara otomatis:
export function PaginatedList() {
const [page, setPage] = useState(1)
const [fetchPosts, { data, isFetching }] = useLazyGetPostsQuery()
return (
<>
<button onClick={() => fetchPosts(page)}>Muat halaman {page}</button>
<button disabled={isFetching} onClick={() => setPage((p) => p + 1)}>
Halaman berikutnya
</button>
</>
)
}useLazyGetPostsQuery memberi kontrol penuh kapan request dijalankan. Argumen page menjadi kunci cache, sehingga kembali ke halaman 2 tidak mengirim request ulang — datanya masih tersimpan.
Untuk daftar tak berujung, buat endpoint yang menerima cursor dan tetap memakai cache sebagai akumulator:
getPostsPaginated: builder.query<PaginatedPosts, number>({
query: (page) => `posts?page=${page}&limit=10`,
serializeQueryArgs: ({ endpointName }) => endpointName,
merge: (currentCache, newItems) => {
currentCache.items.push(...newItems.items)
currentCache.nextPage = newItems.nextPage
},
forceRefetch: ({ currentArg, previousArg }) => currentArg !== previousArg,
}),merge menambahkan halaman baru ke cache yang sama, dan forceRefetch memastikan halaman diambil ulang saat argumen berubah. Kombinasi keduanya menghasilkan infinite scroll tanpa kode manajemen daftar manual.
RTK Query tidak membatasi protokol. Untuk GraphQL, tulis baseQuery custom yang memakai graphql-request:
npm install graphql-request graphqlimport { GraphQLClient } from "graphql-request"
const gqlClient = new GraphQLClient("https://api.example.com/graphql", {
headers: { authorization: "Bearer " + getAccessToken() },
})
export const graphqlBaseQuery = async ({ document, variables }) => {
try {
const data = await gqlClient.request(document, variables)
return { data }
} catch (error) {
return { error }
}
}baseQuery hanya perlu mengembalikan { data } atau { error }. GraphQL ditangani sepenuhnya oleh graphql-request, sementara caching dan lifecycle tetap ditangani RTK Query.
Untuk update real-time, misalnya harga saham atau notifikasi, kombinasikan subscription dengan cache update via updateQueryData:
export const pricesApi = createApi({
reducerPath: "pricesApi",
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
endpoints: (builder) => ({
getPrices: builder.query<Price[], void>({
query: () => "prices",
}),
}),
})
export const onPriceTick = (price: Price) => {
pricesApi.util.updateQueryData("getPrices", undefined, (draft) => {
const idx = draft.findIndex((p) => p.symbol === price.symbol)
if (idx !== -1) draft[idx] = price
})
}Panggil onPriceTick dari koneksi WebSocket. Data langsung memutasi cache tanpa request ulang — UI merender harga terbaru secara otomatis.
RTK Query menyediakan prefetch yang mengirim request lebih awal. Prioritas diatur lewat argumen kedua: force untuk selalu mengambil ulang, ifOlderThan untuk melewatkan cache yang masih segar:
export const prefetchNextPage = () => {
postsApi.util.prefetch("getPostsPaginated", 3, { ifOlderThan: 30000 })
}ifOlderThan: 30000 artinya request hanya dikirim bila cache sudah lebih lama dari 30 detik. Ini cara sederhana menyiapkan data halaman berikutnya saat user mengarahkan mouse ke tombol next.
@rtk-query/codegen-openapi menghasilkan seluruh endpoint dari spesifikasi OpenAPI — mengurangi pekerjaan menulis query manual dan menjaga tipe tetap sinkron dengan API:
npm install -D @rtk-query/codegen-openapi
npx @rtk-query/codegen-openapi openapi-config.tsFile config openapi-config.ts menyebutkan file spesifikasi, lokasi output, dan nama api client. Hasilnya: file TypeScript berisi createApi, semua endpoint, dan tipe data — siap dipakai langsung.
baseQuery juga tempat yang tepat untuk menangani 401. Tangkap error, refresh token, lalu retry request asli:
import { fetchBaseQuery, type BaseQueryFn } from "@reduxjs/toolkit/query/react"
const rawBase = fetchBaseQuery({ baseUrl: "/api" })
export const baseQueryWithAuth: BaseQueryFn<
string | FetchArgs,
unknown,
FetchBaseQueryError
> = async (args, api, extraOptions) => {
let result = await rawBase(args, api, extraOptions)
if (result.error?.status === 401) {
const refresh = await rawBase("/auth/refresh", api, extraOptions)
if (refresh.data) {
setAccessToken(refresh.data.accessToken)
result = await rawBase(args, api, extraOptions)
}
}
return result
}Saat token kedaluwarsa, middleware menjalankan refresh sekali lalu mengulang request awal. Seluruh endpoint yang memakai baseQuery ini otomatis mendapat perilaku sama — tanpa menulis retry di tiap hook.
Tip
Gabungkan custom baseQuery dengan fetchBaseQuery bawaan bila memungkinkan: ia sudah menangani headers, query params, form data, dan error normalisasi. Hanya lapisi dengan logika auth bila benar-benar dibutuhkan.
RTK Query matang untuk skenario dunia nyata: pagination dan infinite scroll via merge serta forceRefetch, GraphQL lewat baseQuery custom, streaming dengan updateQueryData, prefetch berprioritas, endpoint yang di-generate dari OpenAPI, dan refresh token terpusat. Fitur-fitur ini menjadikan RTK Query lapisan data yang hampir tidak perlu ditulis manual.
Inti yang harus dibawa pulang:
merge dan forceRefetch.util.updateQueryData terhadap cache yang ada.prefetch dengan ifOlderThan mengatur prioritas pengambilan data.@rtk-query/codegen-openapi meng-generate endpoint dan tipe dari OpenAPI.Episode 18 berikutnya membahas testing — kalian akan menguji reducer secara murni, menguji async thunk dengan mock fetch, merender komponen dengan store uji memakai testing-library, dan mengintegrasikan MSW untuk mem-mock API secara realistis.