Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/invalidate-queries-meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@tanstack/query-core": minor
---

Forward caller-provided `meta` from `invalidateQueries` options into the `invalidate` action payload visible in `queryCache.subscribe`.

```ts
queryClient.invalidateQueries(
{ queryKey: ['orders'] },
{ meta: { source: 'websocket', traceId: 'abc123' } },
)

queryCache.subscribe((event) => {
if (event.type === 'updated' && event.action.type === 'invalidate') {
console.log(event.action.meta) // { source: 'websocket', traceId: 'abc123' }
}
})
```
28 changes: 28 additions & 0 deletions docs/framework/react/guides/query-invalidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,31 @@ const todoListQuery = useQuery({
```

[//]: # 'Example5'

## Tracing invalidation sources with `meta`

Pass a `meta` object as the second argument to `invalidateQueries` to attach caller context to the invalidation. The value flows through to the `invalidate` action seen by `queryCache.subscribe` subscribers, making it possible to build structured logs or suppress echo-invalidations without any out-of-band bookkeeping.

[//]: # 'ExampleMeta'

```tsx
// Attach a source tag when invalidating from a WebSocket message
queryClient.invalidateQueries(
{ queryKey: ['orders'] },
{ meta: { source: 'websocket', traceId: 'abc123' } },
)

// Read it back in a cache subscriber
queryCache.subscribe((event) => {
if (event.type === 'updated' && event.action.type === 'invalidate') {
analytics.track('cache.invalidated', {
queryKey: event.query.queryKey,
...event.action.meta,
})
}
})
```

[//]: # 'ExampleMeta'

> **Note:** `meta` in the second argument (`options.meta`) is unrelated to `filters.meta` in the first argument. `filters.meta` selects _which_ queries to invalidate; `options.meta` is arbitrary context that rides along with the invalidation action.
3 changes: 3 additions & 0 deletions docs/reference/QueryClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,9 @@ await queryClient.invalidateQueries(
- Defaults to `true`
- Per default, a currently running request will be cancelled before a new request is made
- When set to `false`, no refetch will be made if there is already a request running.
- `meta?: QueryMeta`
- Optional metadata to attach to the invalidation. The value is forwarded to the `invalidate` action payload visible in `queryCache.subscribe`, allowing cache subscribers to identify the source of the invalidation.
- Note: this is distinct from `filters.meta`, which filters queries _by_ their meta.

## `queryClient.refetchQueries`

Expand Down
21 changes: 21 additions & 0 deletions packages/query-core/src/__tests__/queryClient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1600,6 +1600,27 @@ describe('queryClient', () => {
expect(queryFn).toHaveBeenCalledTimes(1)
unsubscribe()
})

it('should forward meta to the invalidate action in queryCache.subscribe', async () => {
const key = queryKey()
await queryClient.prefetchQuery({ queryKey: key, queryFn: () => 'data' })

const events: Array<unknown> = []
const unsubscribe = queryCache.subscribe((event) => {
if (event.type === 'updated' && event.action.type === 'invalidate') {
events.push(event.action.meta)
}
})

await queryClient.invalidateQueries(
{ queryKey: key },
{ meta: { source: 'websocket', traceId: 'abc123' } },
)

unsubscribe()
expect(events).toHaveLength(1)
expect(events[0]).toEqual({ source: 'websocket', traceId: 'abc123' })
})
})

describe('resetQueries', () => {
Expand Down
5 changes: 3 additions & 2 deletions packages/query-core/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ interface ErrorAction<TError> {

interface InvalidateAction {
type: 'invalidate'
meta?: QueryMeta
}

interface PauseAction {
Expand Down Expand Up @@ -388,9 +389,9 @@ export class Query<
)
}

invalidate(): void {
invalidate(meta?: QueryMeta): void {
if (!this.state.isInvalidated) {
this.#dispatch({ type: 'invalidate' })
this.#dispatch({ type: 'invalidate', meta })
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/query-core/src/queryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ export class QueryClient {
): Promise<void> {
return notifyManager.batch(() => {
this.#queryCache.findAll(filters).forEach((query) => {
query.invalidate()
query.invalidate(options.meta)
})

if (filters?.refetchType === 'none') {
Expand Down
4 changes: 3 additions & 1 deletion packages/query-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,9 @@ export interface RefetchQueryFilters<
TQueryKey extends QueryKey = QueryKey,
> extends QueryFilters<TQueryKey> {}

export interface InvalidateOptions extends RefetchOptions {}
export interface InvalidateOptions extends RefetchOptions {
meta?: QueryMeta
}
export interface ResetOptions extends RefetchOptions {}

export interface FetchNextPageOptions extends ResultOptions {
Expand Down
Loading