Stockio

TanStack Query Keys and Invalidation in React

Build predictable React server-state caching with TanStack Query by designing complete query keys, choosing freshness rules, and invalidating after mutations.

By Daniel Kim

React Developer ·

On this page

A query cache is a model of remote information, not merely a way to avoid another fetch. Its keys decide which results are interchangeable. Its freshness policy decides when another observation is useful. Its invalidation rules decide what the interface believes after a mutation. Get those contracts right before tuning request counts.

Model the resource and its identity

Start with the exact inputs that change the server response: account, filters, pagination, locale, and permissions where relevant. Query identity must include the changing inputs used by the query function. Otherwise two different views can accidentally share one cache entry.

Inspect sanitized responses in the JSON Formatter. Compare list and detail shapes instead of assuming they contain identical records. A list may omit fields, include aggregates, or reflect a different permission scope. Those differences influence whether direct cache updates are safe.

TanStack Query is intended for server state. Keep transient menu state and unsaved drafts elsewhere. A query response is a snapshot from an external authority; an editor draft is a local proposal. Copying every response into a global client store creates two caches with no clear reconciliation policy.

Build complete and stable keys

Use arrays with a consistent hierarchy. Put the broad resource first, then its scope and parameters. A key factory can make related queries easier to invalidate without duplicating string conventions across components. Include every value that changes the response, but do not include incidental object identity or a callback.

import { useQuery } from '@tanstack/react-query';

const projectKeys = {
list: (accountId: string, page: number) =>
  ['projects', accountId, { page }] as const,
};

export function useProjects(accountId: string, page: number) {
return useQuery({
  queryKey: projectKeys.list(accountId, page),
  queryFn: async ({ signal }) => {
    const params = new URLSearchParams({ accountId, page: String(page) });
    const response = await fetch('/api/projects?' + params, { signal });
    if (!response.ok) throw new Error('Project request failed');
    return response.json();
  },
  staleTime: 30_000,
});
}

The server still needs to authorize account access. A cache key is not an access-control mechanism. The response also needs runtime validation when its shape cannot be trusted; response.json alone does not provide it. Complete keys prevent data collisions, not malicious access.

The TanStack Query documentation describes key hashing and supported values. Prefer a small, serializable parameter object. Normalize equivalent filters deliberately: an omitted status and an explicit all status should either share identity or intentionally remain separate.

Separate freshness from retention

staleTime describes how long cached data is considered fresh. gcTime describes how long an inactive query can remain before garbage collection. They solve different problems. A long retention window does not guarantee that a query will avoid refetching when it becomes active again.

Choose freshness budgets from the product contract. A country list may tolerate a much longer interval than a shared task board. A balance display may need a deliberate refresh after a transaction regardless of the normal stale interval. No single number is correct for every endpoint.

Stale does not mean the interface must discard its existing data. It means the data is eligible for refetch under the library's configured triggers. Window focus, reconnecting, and mounting can influence that behavior. Consult the important defaults guide before interpreting a development request as a duplicate bug.

Avoid turning every automatic trigger off at the first unexpected request. First identify which trigger fired and whether the query key changed. Disabling all refresh behavior can make a shared application silently outdated. Document the unusual cases where a manual-only policy is intentional.

Invalidate according to domain consequences

After creating a project, invalidate lists that can now include it. After changing its title, consider detail views, search results, and summary widgets. Mutation consequences rarely stop at the button that submitted the request. Write down affected query families before choosing direct cache manipulation.

Invalidation marks matching queries stale and ordinarily refetches active matches according to the API and options. It is not the same as removing every cached record. Narrow matching avoids unnecessary work, while broad resource-level invalidation is a reasonable starting point when correctness is more important than fine tuning.

Directly setting cached data can be effective when the mutation returns an authoritative record. It is harder for paginated, filtered, or sorted lists: a title change may move a record out of one result set and into another. Do not append a result to every cached list unless those inclusion rules are actually known.

Use the invalidation guide for version-specific behavior. Await invalidation when the interaction must remain pending until affected views are refreshed. Otherwise be explicit that the operation is accepted while related data may update afterward. Neither choice should happen accidentally because a promise was forgotten.

Test cache behavior as user behavior

Exercise two accounts, several filter combinations, and back navigation. Confirm that a result for one scope never appears in another, including during transitions. Clear or replace relevant client caches when authentication identity changes. Session isolation includes cached browser data, not only server authorization.

Test a list, a detail view, and a mutation together. Assert that the updated record is visible after navigating back and that removed records do not reappear from an old cache entry. Use a new QueryClient for independent tests so earlier fixtures do not influence later expectations.

Inspect request timing rather than only the final screen. A component may look correct while fetching sequentially or refetching because its parameters are unstable. The development collection and React collection provide related implementation references, but the cache policy should remain documented beside the resource hooks.

Finally, keep a distinction between acceptable staleness and incorrect identity. A record that is thirty seconds old may satisfy the product contract. A current record from another account never does. Correct scope comes before reducing network traffic or extending freshness intervals.

Trace a mutation across two filtered views

Imagine an issue detail view and two lists: open issues and issues assigned to you. Closing an issue changes its detail status and removes it from the open list, but may leave it in the assigned list with different summary counts. Updating the detail cache alone cannot maintain all three contracts.

For a first implementation, write the canonical mutation response into the exact detail entry when its shape matches, then invalidate the affected list family. Let the server recompute membership, ordering, and totals. This costs requests but avoids reproducing every server filtering rule in browser code. Narrow invalidation later if measurements justify the complexity.

Test closing the last issue on a paginated page. A page can become empty even though earlier pages still contain records; the interface may need to move back one page after the refreshed result arrives. Also switch filters while the mutation is pending. Completion should target the mutated resource's keys, not whichever filter happens to be visible when the promise resolves.

Key takeaways

  • Include all response-changing inputs in hierarchical query keys.
  • Choose freshness separately from inactive retention and understand automatic refetch triggers.
  • Invalidate according to domain consequences, and test account changes, navigation, filters, and mutations together.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in JavaScript & Frameworks
JavaScript & Frameworks

10 Best JavaScript Charting Libraries

Compare ten JavaScript chart libraries for dashboards, reports, and interactive data stories. See where each excels, what it costs, and how to choose based on your app and team.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

8 Best React State Management Libraries

Find a React state approach that fits your app, from built-in hooks to dedicated stores, with clear trade-offs around data flow, debugging, async work, and learning effort.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

12 Best and Most Popular JavaScript Libraries

Compare twelve widely used JavaScript libraries for interfaces, data, testing, and everyday development, with clear strengths, trade-offs, and advice for choosing only what your project needs.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

localStorage vs sessionStorage vs IndexedDB Compared

Choose browser storage by data lifetime, size and access pattern. Compare synchronous Web Storage with IndexedDB transactions, migrations, quotas and failure handling.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

React Server Components Explained for Developers

Learn what React Server Components execute, what crosses the client boundary, and how to structure data access, interactive widgets, and mutations without leaking private data.

Includes: JSON formatterDaniel KimDaniel Kim
JavaScript & Frameworks

useMemo vs useCallback vs React.memo: Fewer Rerenders

Learn when React memoization helps, how prop identity affects rerenders, and why profiling and state placement should come before useMemo and useCallback.

Includes: JSON formatterDaniel KimDaniel Kim