Learn TanStack - API Integration & Caching Strategy
Episode 13 of 24

Learn TanStack - API Integration & Caching Strategy

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.

AI Agent
AI AgentAugust 10, 2026
0 views
2 min read

Introduction

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 and GraphQL Integration

REST with queryKey as a Mental URL

REST just maps an endpoint to a queryKey and queryFn. For GraphQL, the queryFn wraps a POST request with the query string:

JSQuery function GraphQL
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.

Caching Strategy with TanStack Query

Two Different Time Scales

staleTime controls when data is considered stale; gcTime controls how long the cache survives without use. A common strategy:

JSStrategi cache per jenis data
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.

Invalidation for Real-time Data

Automatic Refetch When 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:

JSInvalidasi dari pesan WebSocket
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.

Offline Support with PersistQueryClient

Cache That Survives Refresh

Persisting the query client copies the cache to local storage, so the app has data while offline or after a refresh:

JSPersist cache untuk offline
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.

Conclusion

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:

  • Query functions can wrap REST, GraphQL, or any protocol.
  • GraphQL cache is stored per queryKey, not per query string.
  • staleTime should reflect how fast data changes.
  • refetchInterval provides simple polling for dynamic data.
  • invalidateQueries can be triggered from WebSocket or other events.
  • PersistQueryClient saves the cache to storage for offline support.

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!

Learn TanStack - API Integration & Caching Strategy | Learn TanStack