How it works
From mutation to UI update
A server action fires. Six steps later, every subscribed browser tab shows fresh data. Here's exactly how it flows.
The big picture
End-to-end data flow
Scroll to reveal each stage. The entire pipeline happens in milliseconds.
1Server MutationcreateServerFn()
2invalidateTags()Tag dispatch
3Tag ResolutionGroup to children
4SSE BroadcastEventSource push
5Client ReceivesEvent handler
6Tag MatchingActive queries
7Query RefetchqueryClient
8React Re-renderFresh data
1Server MutationcreateServerFn()
2invalidateTags()Tag dispatch
3Tag ResolutionGroup to children
4SSE BroadcastEventSource push
5Client ReceivesEvent handler
6Tag MatchingActive queries
7Query RefetchqueryClient
8React Re-renderFresh data
Step by step
Six steps explained
Click any step to expand the explanation and code example.
What's happening
The tag tree is your contract between server and client. Parent nodes are group tags — calling appTags.todos() returns a tag that matches all todos.* queries. Leaf nodes return specific tags for single-item precision. TypeScript infers the full tree.
Code
import { function defineTags<T extends ValidTagFactoryTree<T>>(factories: T): ResolvedTagTree<T>Builds a typed, callable tag tree from a plain factory object. Each leaf becomes a
tag factory stamped with its `TAG_PATH`; each branch becomes a callable `TagGroup` that
expands to all descendant leaves when invoked without arguments.defineTags } from "@tanstack-tools/query-tags"
export const const appTags: ResolvedTagTree<{
todos: {
list: () => string[];
summary: () => string[];
byId: (id: string) => string[];
};
notes: {
list: () => string[];
};
}>
appTags = defineTags<{
todos: {
list: () => string[];
summary: () => string[];
byId: (id: string) => string[];
};
notes: {
list: () => string[];
};
}>(factories: {
todos: {
list: () => string[];
summary: () => string[];
byId: (id: string) => string[];
};
notes: {
list: () => string[];
};
}): ResolvedTagTree<{
todos: {
list: () => string[];
summary: () => string[];
byId: (id: string) => string[];
};
notes: {
list: () => string[];
};
}>
Builds a typed, callable tag tree from a plain factory object. Each leaf becomes a
tag factory stamped with its `TAG_PATH`; each branch becomes a callable `TagGroup` that
expands to all descendant leaves when invoked without arguments.defineTags({
todos: {
list: () => string[];
summary: () => string[];
byId: (id: string) => string[];
}
todos: {
// group tag — invalidates all children
list: () => string[]list: () => ["todos"],
summary: () => string[]summary: () => ["todos", "summary"],
byId: (id: string) => string[]byId: (id: stringid: string) => ["todo", id: stringid],
},
notes: {
list: () => string[];
}
notes: {
list: () => string[]list: () => ["notes"],
},
})
// Usage — fully typed:
const appTags: ResolvedTagTree<{
todos: {
list: () => string[];
summary: () => string[];
byId: (id: string) => string[];
};
notes: {
list: () => string[];
};
}>
appTags.todos: () => TagGrouptodos() // invalidates list + summary + byId(*)
const appTags: ResolvedTagTree<{
todos: {
list: () => string[];
summary: () => string[];
byId: (id: string) => string[];
};
notes: {
list: () => string[];
};
}>
appTags.todos: TagNode<{
list: () => string[];
summary: () => string[];
byId: (id: string) => string[];
}>
todos.list: () => string[]list() // ["todos"] only
const appTags: ResolvedTagTree<{
todos: {
list: () => string[];
summary: () => string[];
byId: (id: string) => string[];
};
notes: {
list: () => string[];
};
}>
appTags.todos: TagNode<{
list: () => string[];
summary: () => string[];
byId: (id: string) => string[];
}>
todos.byId: (id: string) => string[]byId("1") // ["todo", "1"] onlyWant to see which queries match which tags? Try the interactive Tag Playground →
Next step
Ready to set it up?
The guide walks through every setup step with working code examples.