This episode connects TanStack Query to various backends: REST and GraphQL integration, caching strategy with staleTime and gcTime, invalidation for real-time data with polling, and offline support using a persisted query client.

A query function is just a function that returns a promise — which means TanStack Query can talk to any backend: REST, GraphQL, WebSocket, even local services. Episode 13 connects all of these with the right cache strategy.
Episode 13 covers REST and GraphQL integration, caching strategy with staleTime and gcTime, invalidation for real-time data, and offline support with a persisted query client.
The end goal is simple: fast data, always fresh, and an app that stays useful even when the connection drops. All of it is achieved through cache settings, not a new architecture.
REST just maps an endpoint to a queryKey and queryFn. For GraphQL, the queryFn wraps a POST request with the query string:
const graphQLFetcher = async ({ query, variables }) => {
const res = await fetch("/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
})
const json = await res.json()
if (json.errors) throw new Error(json.errors[0].message)
return json.data
}
const { data } = useQuery({
queryKey: ["proyek", filter],
queryFn: () =>
graphQLFetcher({
query: "query Proyek($filter: String) { proyek(filter: $filter) { id nama } }",
variables: { filter },
}),
})graphQLFetcher sends the query and variables, then throws an error if GraphQL returns errors. Notice the queryKey includes filter — changing the filter means a new query, so the cache per variable stays organized.
staleTime controls when data is considered stale; gcTime controls how long the cache survives without use. A common strategy:
const { data: profil } = useQuery({
queryKey: ["profil"],
queryFn: ambilProfil,
staleTime: 5 * 60_000,
})
const { data: hargaSaham } = useQuery({
queryKey: ["harga"],
queryFn: ambilHarga,
staleTime: 5_000,
refetchInterval: 5_000,
})A profile that rarely changes uses a staleTime of five minutes — almost no refetching. Dynamic stock prices use a staleTime of five seconds plus refetchInterval: 5_000 to stay synced. The key: match staleTime to how fast the data changes.
Besides polling, invalidation can be triggered from external events — for example, a WebSocket notifying about new data. Invalidation only marks queries as stale; the refetch runs according to each query's own policy:
socket.onmessage = (event) => {
const pesan = JSON.parse(event.data)
if (pesan.tipe === "data-baru") {
queryClient.invalidateQueries({ queryKey: ["laporan"] })
}
}invalidateQueries({ queryKey: ["laporan"] }) marks all queries starting with laporan as stale. If a page is currently showing that data, a refetch runs automatically; if not, it's just flagged until the page opens again — efficient, without wasting requests.
Persisting the query client copies the cache to local storage, so the app has data while offline or after a refresh:
import { persistQueryClient } from "@tanstack/react-query-persist-client"
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"
const persister = createSyncStoragePersister({
storage: window.localStorage,
})
persistQueryClient({
queryClient,
persister,
maxAge: 24 * 60 * 60 * 1000,
})createSyncStoragePersister({ storage: window.localStorage }) saves the cache to localStorage. maxAge: 24 * 60 * 60 * 1000 caps restored cache at a maximum of one day. While offline, the app shows stored data; when online, stale queries are refetched.
Warning
Be careful storing sensitive data in localStorage through the persister. Limit maxAge and filter queryKeys so secret data isn't persisted longer than necessary.
Episode 13 wrapped up API integration and cache strategy: REST and GraphQL work through the same query functions, staleTime and gcTime are matched to data dynamics, invalidation from real-time events keeps data fresh, and persisted cache keeps the app alive while offline.
Key takeaways:
In the next episode, episode 14, we'll discuss best practices and observability — logging the query lifecycle, monitoring render cost, debugging TanStack Query and Router with DevTools, and instrumentation for production. It's time to make sure your app is healthy!