skip to content

A TanStack Query v5 report keyed ['report', { from: new Date(), filters }] keeps sending requests and rarely shows data; what is wrong with that key?

level: seniorimportance: should knowfreq 32%

answer

  1. hashed by value, every render
  2. a timestamp is never the same twice
  3. each response triggers a render
  4. some values serialize to {}
  5. compute the value once

basics

~20 s

new Date() serializes to a new timestamp on every render, so each render builds a new key, misses the cache and sends a request. Compute the date once, in state or as a day string.

solid answer

~50 s

The key is hashed by value on every render, and `new Date()` serializes to an ISO timestamp with millisecond precision. Each render therefore produces a different `queryHash`, which means a new cache entry that is pending with no data, and a new fetch. When that fetch resolves, the component re-renders, the render creates yet another timestamp, and the hook moves to another empty entry before the data can be shown. Any unrelated parent re-render does the same. The abandoned entries remain in the cache until they are garbage-collected. The fix is to make the key a pure function of the inputs that define the data: hold the date in state, round it to a stable unit such as `'2026-09-25'`, or derive it from the user's selected range. Similar traps are values that do not serialize faithfully, such as a `Map` or `Set`, which become `{}` and make different filters collide.

code

tsx · 29 lines
tsx
import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'

type Filters = { region: string; channels: string[] }
type Report = { total: number }

async function fetchReport(from: string, filters: Filters): Promise<Report> {
  const params = new URLSearchParams({ from, region: filters.region })
  filters.channels.forEach((c) => params.append('channel', c))
  const res = await fetch(`/api/report?${params}`)
  if (!res.ok) throw new Error('Failed to load report')
  return res.json()
}

const today = () => new Date().toISOString().slice(0, 10) // e.g. '2026-09-25'

export function useReport(filters: Filters) {
  // Broken: a new timestamp, and therefore a new key, on every render.
  // useQuery({ queryKey: ['report', { from: new Date(), filters }], ... })

  // Fixed: the date is computed once and rounded to the unit the data uses.
  const [from] = useState(today)
  const channels = [...filters.channels].sort()

  return useQuery({
    queryKey: ['report', { from, region: filters.region, channels }],
    queryFn: () => fetchReport(from, { ...filters, channels }),
  })
}

go deeper

for a junior

Remember that a query key must describe the data, not the moment of rendering: a value that is different on every render means a new cache entry and a new request every time.

for a middle

Explain the loop: the key is hashed by value each render, a new timestamp is a new entry, the response re-renders the component, and the next key misses again.

for a senior

Diagnose from the network tab and Devtools, and fix by computing time values once and rounding them. Also catch the opposite failure, where a Map or Set collapses different filters into one entry.

for a principal

Make key values a reviewed contract, plain data built by one factory, so that unstable or lossy values are caught in review instead of in production memory graphs.

## What the key does on every render TanStack Query hashes the **query key** on every render to find the cache entry the hook should observe. The default `hashKey` is `JSON.stringify` with plain-object properties sorted, so equality is by **serialized value**. That is what makes an inline key such as `['report', { filters }]` safe even though the array is new each time. The flip side is that any value whose serialization **changes between renders** turns every render into a different key. ## Why new Date() breaks it A `Date` serializes through its `toJSON` method to an ISO string with **millisecond precision**, for example `"2026-09-25T10:15:04.217Z"`. Created during render, it produces a new string almost every time. The loop runs like this: 1. Render 1 builds a key with timestamp A. There is no entry for it, so the hook reports `pending` with no data and starts a fetch. 2. The response for A arrives, and the hook's result changes, so the component re-renders. 3. Render 2 builds a key with timestamp B. That is a **different entry**, pending with no data, so the hook shows the loading state again and starts another fetch. 4. The response for B arrives, the component re-renders, and the cycle repeats. The data for A was fetched and cached correctly. It is simply never on screen, because the hook has already moved on to B. An unrelated parent re-render, such as a clock or a hover state, starts the same cycle. ## Symptoms - The network tab shows the same endpoint called over and over, each request identical except for the timestamp. - The component stays in, or keeps returning to, its loading state. - The library's Devtools lists a growing number of `['report', …]` entries with different timestamps, most of them inactive. - Memory use grows while the screen stays open, because abandoned entries stay cached until garbage collection removes them. ## Values that break keys The same reasoning covers every value whose serialization does not track the data it stands for: | Value in the key | What the default hash produces | Effect | |---|---|---| | `new Date()` or `Date.now()` taken during render | a new string or number each millisecond | a new entry on nearly every render | | a `Map` or `Set` of filters | `{}` | different filters **collide** on one entry | | a function inside an object | the property is dropped | different callbacks collide | | a class instance | its own enumerable fields, in insertion order and unsorted | fragile, order-dependent keys | | a `BigInt` | `JSON.stringify` throws | the render fails | | `undefined` inside an array | `null`, keeping its slot | differs from leaving the value out | Note the two opposite failures. Unstable values cause **too many** entries, and lossy values such as a `Map` cause **too few**, silently serving one filter's data for another. ## Fixes - **Compute time-based values once.** Hold the start of the range in state (`useState(() => startOfToday())`), or derive it from what the user selected. Never evaluate "now" inside the key. - **Round to the unit the data actually uses.** A daily report keyed by `'2026-09-25'` changes once a day, not once a millisecond. - **Serialize collections yourself.** Convert a `Set` to a sorted array, and a `Map` to a plain object or an array of pairs, before putting it in the key. - **Keep keys to plain data.** Use strings, numbers, booleans, `null`, arrays and plain objects, which is what the docs mean by a serializable key. - **Centralize construction** in a key factory, so one function decides how a date range or a filter set turns into key values. ## queryKeyHashFn as a last resort A query, or the client defaults, can supply **`queryKeyHashFn`** to replace the default hashing, for instance to encode a `Map` deterministically. It does not fix an unstable value: a timestamp is still different on every render whatever function hashes it. It also makes every key depend on custom code. Converting the values to plain data before they reach the key is almost always the simpler and safer fix.

  • Would a longer staleTime stop the repeated requests?
    No. `staleTime` applies to a single entry and decides when that entry's data counts as stale. Here every render creates a different entry that has never been fetched, so there is nothing fresh to reuse. The key has to stop changing before any caching setting can help.
  • How would you confirm this diagnosis quickly?
    Log the key, or `hashKey(key)`, on each render, or watch the library's Devtools. If the hash differs between renders while the user changed nothing, a value inside the key is unstable. The request URLs in the network tab usually show the same thing, differing only in a timestamp parameter.

saying these in an interview costs you the question

  • TanStack Query compares keys by reference, like a useEffect dependency array.
  • A Map is a safe way to pass a set of filters in a key.
  • A longer staleTime will stop the repeated requests.
  • Abandoned entries leave the cache the moment nothing observes them.
  • A custom queryKeyHashFn fixes a timestamp that changes every render.