---
title: "TanStack Query Data Flow: Keys, Invalidation, and Queries"
description: Trace ChatCrystal hooks and API, verify parameterized query keys and prefix invalidation, and distinguish accepted requests from background refreshes.
image: "https://zengliangyi.com/og/blog/en/tanstack-query-data-flow.png"
---

List filters, note details, and background summarization change page data at different times. A query library manages server state, but cache identity and completion events still need correct wiring to refresh the interface. This article reads ChatCrystal’s current query paths and tests cache behavior without React, HTTP, or real data.

## Separate query cache from interface state

[App.tsx](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/client/src/App.tsx) creates a top-level QueryClient and supplies it through QueryClientProvider. Current query defaults are staleTime 30,000ms and retry 1. These are not universally best settings, and 30 seconds of freshness do not mean server data cannot change.

Query results are server state. Search input, selected tags, page number, and dialog visibility are interface state; uniformity alone does not justify putting them all in query cache. The current notes page passes filters to useNotes and resets pagination when filters change, avoiding an offset inherited from another selection.

## Cache keys must cover request parameters

[use-notes.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/client/src/hooks/use-notes.ts) identifies lists with `['notes', params]` and details with `['note', id]`. Tags, search, source kind, offset, and limit affect request identity. Using only `['notes']` for every page would put distinct pages in one cache record.

Object property order does not create a different key; array element order still matters. The detail hook enables automatic queries only when id > 0. Fractions or Infinity can also satisfy that condition, so it is not complete ID validation and does not establish that TypeScript has narrowed arbitrary values or undefined into a valid note.

The following uses query-core 5.102.8, matching the current React Query version. Install in a new temporary directory, save `cache.mjs`, and run with Node 24+. **This offline cache experiment has no observers**, React page, or HTTP request. It disables retry and automatic collection for determinism, differing from application defaults.



```sh
npm   init   -y  
  npm   install   --ignore-scripts   --no-audit   --no-fund   @tanstack/query-core@5.102.8
```





```js
import   assert   from   'node:assert/strict'  ;  
  import   { QueryClient }   from   '@tanstack/query-core'  ;  
  
  const   client   =   new   QueryClient  ({  
    defaultOptions: { queries: { staleTime:   30_000  , retry:   false  , gcTime:   Infinity   } },  
  });  
  let   reads   =   0  ;  
  const   queryFn   =   async   ()   =>   ({ revision:   ++  reads });  
  const   first   =   [  'notes'  , { tag:   'mcp'  , offset:   0  , limit:   20   }];  
  const   reordered   =   [  'notes'  , { limit:   20  , offset:   0  , tag:   'mcp'   }];  
  const   second   =   [  'notes'  , { tag:   'mcp'  , offset:   20  , limit:   20   }];  
  try   {  
    assert.  equal  ((  await   client.  fetchQuery  ({ queryKey: first, queryFn })).revision,   1  );  
    assert.  equal  ((  await   client.  fetchQuery  ({ queryKey: reordered, queryFn })).revision,   1  );  
    await   client.  fetchQuery  ({ queryKey: second, queryFn });  
    assert.  equal  (reads,   2  );  
    client.  setQueryData  ([  'note'  ,   7  ], { title:   'separate detail'   });  
    await   client.  invalidateQueries  ({ queryKey: [  'notes'  ] });  
    assert.  equal  (reads,   2  );  
    assert.  equal  (client.  getQueryState  (first).isInvalidated,   true  );  
    assert.  equal  (client.  getQueryState  (second).isInvalidated,   true  );  
    assert.  equal  (client.  getQueryState  ([  'note'  ,   7  ]).isInvalidated,   false  );  
    assert.  equal  ((  await   client.  fetchQuery  ({ queryKey: first, queryFn })).revision,   3  );  
    console.  log  (  'keys, cached reads, and inactive invalidation pass'  );  
  }   finally   {  
    client.  clear  ();  
  }
```



Change invalidateQueries’ prefix from notes to note and the list invalidation assertion should fail; restore it and the program passes. Both offsets match the list prefix, while the detail does not. Without active observers, invalidation does not immediately increase reads; the next fetchQuery reads again. Active queries in real pages normally also refetch in the background when invalidated; see the [official invalidation guide](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation).

## API failures must become query failures

The current [api.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/client/src/lib/api.ts) wrapper sets Content-Type when needed, attaches a configured token as a Bearer header, and turns HTTP failures and business success:false into exceptions. HTTP 401 also clears the token and notifies authentication state listeners. A generic return type is a type assertion, rather than runtime validation of arbitrary response JSON.

The wrapper exposes failures to queries, but pages still need to display them. For example, [SearchPage](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/client/src/pages/SearchPage.tsx) handles isError. The current notes list mainly consumes data and isLoading, so a display without data is not necessarily a confirmed empty list. Investigate initial loading, background refresh, failure, and actual zero results separately.

useMutation is not assigned by HTTP method either. SearchPage uses it for a button-triggered GET search. Statements that every read must use useQuery or every mutation writes server data do not match current code. Choose according to cache and trigger requirements rather than names alone.

## Handle acceptance separately from completion

On request success, the current summarization mutation invalidates queue-status, conversation, conversations, notes, status, and related queries. Batch summarization and cancellation have their own key sets. But accepting a queued request can leave AI work running. Refreshing notes at that point does not establish that the new note is already available.

[use-queue.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/client/src/hooks/use-queue.ts) polls every 2 seconds with active work and every 10 seconds otherwise. [StatusBar](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/client/src/components/StatusBar.tsx) displays progress and completion. Its completion notice does not invalidate notes, so it does not establish that all lists automatically synchronize when background work ends. See [task queue design](/en/blog/pqueue-task-queues/) for the queue’s own boundaries.

The import [SSE hook](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/client/src/hooks/use-import-stream.ts) follows a separate fetch path. On done, it updates import state and invalidates conversations and status. SSE completion, queue completion, and ordinary request success are distinct events. Trace the cache keys each event should refresh.

## Make refresh behavior testable

An improvement could invalidate relevant keys from explicit completion events according to task results, then verify that a delayed fixture task actually produces a visible new note. **This is a proposal, not a change implemented by this article.** Optimistic updates likewise need rollback and concurrency policies; invalidateQueries alone does not establish that they exist.

Acceptance checks should distinguish whether filters select a new key, work remains active after request success, completion triggers another read, and failures display errors rather than empty results. Choose freshness and polling intervals around these events. The offline read counts here do not measure network performance or page response time.

Share:

##  Related Posts

[##  Five AI conversation parsers: retained fields and loss boundaries

Build an acceptance matrix from Claude Code, Codex, Cursor, Trae, and Copilot parsers, checking tool flags, thinking, order, and snapshot coverage.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#TypeScript

#Node.js](/en/blog/ai-conversation-parser-boundaries)    [##  AI SDK Provider Abstraction: Capabilities and Protocols

Test 7 ChatCrystal providers and AI SDK factories with offline mocks for Responses, Chat Completions, embeddings, and structured output rejection.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#TypeScript](/en/blog/ai-sdk-provider-abstraction)    [##  SourceAdapter internals: identity, content hashes, and reimport

Trace ChatCrystal’s registry, importer, and remote payload to verify identity, canonical hashes, metadata shortcuts, and parser-version boundaries.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#TypeScript

#Node.js](/en/blog/source-adapter-internals)

##  From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.

[Visit my GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "TanStack Query Data Flow: Keys, Invalidation, and Queries",
  "description": "Trace ChatCrystal hooks and API, verify parameterized query keys and prefix invalidation, and distinguish accepted requests from background refreshes.",
  "url": "https://zengliangyi.com/en/blog/tanstack-query-data-flow/",
  "image": "https://zengliangyi.com/images/blog/tanstack-query-data-flow.svg",
  "datePublished": "2026-10-06T00:00:00.000Z",
  "dateModified": "2026-10-23T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/en/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/en/blog/tanstack-query-data-flow/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://zengliangyi.com/en"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://zengliangyi.com/en/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "TanStack Query Data Flow: Keys, Invalidation, and Queries",
      "item": "https://zengliangyi.com/en/blog/tanstack-query-data-flow/"
    }
  ]
}
```
