This episode equips your TanStack app with eyes: monitoring data fetching and rendering on the client side, tracking cache behavior and query performance, error reporting for the UI and data layers, and performance and user experience analytics.

An app deployed without observability is a black box: you only find out something's wrong when users report it. Episode 21 covers observability and monitoring for TanStack apps: monitoring data fetching and rendering on the client side, tracking cache behavior and query performance, error reporting for the UI and data layers, and user experience analytics.
TanStack Query already provides the observability stage through the query cache, events, and callback hooks like onError and onSuccess. Your job is to capture those signals and forward them to a monitoring system so problems are detected earlier.
By the end of the episode, your app will send enough telemetry to answer the questions: which data failed to load, how long did it take, and does the user feel the app is slow.
TanStack Query publishes events at every stage of a query: fetch started, succeeded, failed, and data expired. Those events can be used to send metrics to a monitoring backend.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryCacheNotify: true,
},
},
})
queryClient.getQueryCache().subscribe((event) => {
if (event.type === "queryUpdated") {
trackMetric(event.query.queryKey.join("."), event.query.state.status)
}
})queryClient.getQueryCache().subscribe(event => ...) lets you listen to all cache changes from one place. The query key becomes the metric name and the status becomes the recorded value, so the dashboard immediately shows which queries fail often.
Wasteful rendering can be spotted with the React Profiler and render counters. For large TanStack Table apps, make sure state changes don't re-render the whole table — column visibility and sorting should run efficiently with state scoped per feature.
TanStack Query provides queryClient.getQueryCache().getAll() to peek at the entire cache contents: how many queries, how old they are, and when they were last fetched.
const queries = queryClient.getQueryCache().getAll()
const ringkasan = queries.reduce((acc, query) => {
acc[query.state.status] += 1
return acc
}, { error: 0, success: 0, pending: 0 })queryClient.getQueryCache().getAll() returns all queries in the cache with their statuses. The ringkasan summary can be sent periodically to monitoring to see the health of the app's cache in aggregate.
Measure fetch duration inside queryFn and send it as a histogram. A striking difference between dev and production is often the first signal of a backend problem.
const queryFn = async () => {
const mulai = performance.now()
try {
const data = await ambilData()
trackTiming("query.pengguna", performance.now() - mulai)
return data
} catch (error) {
trackError("query.pengguna", error)
throw error
}
}performance.now() gives millisecond-precision timestamps for measuring duration. With trackTiming and trackError, every query sends two signals: how long it took and whether it succeeded.
Combine the QueryClient default options with an error boundary to catch data failures and render failures in a single strategy.
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
reportError("query", query.queryKey, error)
},
}),
})new QueryCache({ onError }) catches all query errors in one place without changing a single hook. Errors are forwarded to the reporting system, while the UI still shows error state through each page's ErrorBoundary.
Not every failure needs to trigger an alarm. TanStack's automatic retry already handles network fluctuations; report only the errors that persist after retries are exhausted. On the UI side, make sure the error boundary shows a friendly fallback, not a blank page.
Metrics like LCP and INP measure real user experience, not just the performance of a development machine. Send these metrics along with the active page and route context so the team can see which pages are slowest.
export function KirimVitals({ onPerfEntry }) {
useEffect(() => {
if (typeof onPerfEntry !== "function") return
import("web-vitals").then(({ onLCP, onINP, onCLS }) => {
onLCP(onPerfEntry)
onINP(onPerfEntry)
onCLS(onPerfEntry)
})
}, [onPerfEntry])
return null
}The dynamic import import("web-vitals") loads the library only when needed, keeping the bundle small. The onPerfEntry callback sends metrics to analytics along with the currently active route info.
Because TanStack Router handles navigation, it's easy to associate every metric with the route being viewed. Attach the route name to the metric label so analysis goes down to the page level, not just the app level.
Episode 21 wrapped up observability and monitoring: lifecycle events and cache subscription for fetch monitoring, cache statistics and duration measurement for query performance, error reporting for the UI and data layers, and Web Vitals for real user experience.
Key takeaways:
In the next episode, episode 22, we'll discuss stable modern features and trends — the latest stable features across the entire TanStack ecosystem, updates to Router, Table, Query, Virtual, and Charts, trends in headless libraries and data-driven React, and strategies for keeping your TanStack skills relevant. Time to look at the future of this ecosystem!