Install
Add the package and its peer dependency. keyv provides the in-process tag registry.
pnpm add @tanstack-tools/query-tags keyvPeer deps: @tanstack/react-query >=5, @tanstack/react-router >=1, @tanstack/react-start >=1
Quickstart
The shortest path from zero to tag-based invalidation in a TanStack Start app. Install, define tags, tag one query, invalidate once — done.
Prerequisites
Add the package and its peer dependency. keyv provides the in-process tag registry.
pnpm add @tanstack-tools/query-tags keyvPeer deps: @tanstack/react-query >=5, @tanstack/react-router >=1, @tanstack/react-start >=1
Create a typed tag tree once. Parent nodes are group tags — invalidating a parent refreshes all children.
// src/lib/tags.ts
import { defineTags } from "@tanstack-tools/query-tags"
export const appTags = defineTags({
todos: {
list: () => ["todos"],
byId: (id: string) => ["todo", id],
},
})Wrap any useQuery call with withTags(). The runtime picks up the tags automatically — no other changes.
// In your component or query factory
import { useQuery } from "@tanstack/react-query"
import { withTags } from "@tanstack-tools/query-tags"
import { appTags } from "./tags"
export function useTodos() {
return useQuery(
withTags(
{ queryKey: ["todos"], queryFn: fetchTodos },
[appTags.todos.list()],
),
)
}You can also use meta: { tags: [...] } inline. withTags is cleaner for shared query factories.
Instantiate the invalidation system on the server once. Keep this import server-only.
// src/lib/tag-invalidation.ts (server-only)
import { createTagInvalidationSystem } from "@tanstack-tools/query-tags/server"
export const tagInvalidation = createTagInvalidationSystem()This file imports from @tanstack-tools/query-tags/server which uses Node.js APIs. Name it *.server.ts or keep it behind a server function — never import it in client code.
Add a catch-all API route that handles both the SSE stream (GET) and control endpoint (POST).
// src/routes/api.invalidator.$.ts
import { createFileRoute } from "@tanstack/react-router"
import { tagInvalidation } from "#/lib/tag-invalidation"
const handler = tagInvalidation.createAPIHandler({
basePath: "/api/invalidator",
})
async function handle({ request }: { request: Request }) {
return handler(request)
}
export const Route = createFileRoute("/api/invalidator/$")({
server: {
handlers: { GET: handle, POST: handle },
},
})Add TagInvalidationProvider high up in your tree — typically in your root provider file. It opens the SSE connection.
// src/integrations/tanstack-query/root-provider.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { useRouter } from "@tanstack/react-router"
import { TagInvalidationProvider } from "@tanstack-tools/query-tags/react"
import { createInvalidatorClient } from "@tanstack-tools/query-tags/client"
const invalidatorClient = createInvalidatorClient({
basePath: "/api/invalidator",
})
export default function TanStackQueryProvider({
children,
}: {
children: React.ReactNode
}) {
const queryClient = /* your QueryClient instance */
const router = useRouter()
return (
<QueryClientProvider client={queryClient}>
<TagInvalidationProvider
client={invalidatorClient}
queryClient={queryClient}
router={router}
>
{children}
</TagInvalidationProvider>
</QueryClientProvider>
)
}Call invalidateTags() in any server function after a mutation. Every tagged query refetches automatically.
// src/lib/server-functions.ts
import { createServerFn } from "@tanstack/react-start"
import { z } from "zod"
import { tagInvalidation } from "./tag-invalidation"
import { appTags } from "./tags"
export const addTodoServerFn = createServerFn()
.inputValidator(z.object({ name: z.string().min(1) }))
.handler(async ({ data }) => {
await db.insert(todos).values({ name: data.name })
// One call refreshes every query tagged with todos.*
await tagInvalidation.invalidateTags({
tags: [appTags.todos()],
})
})appTags.todos() is the group tag — it expands to all children: list, byId(*), etc.
That's it
From this point, every query tagged with appTags.todos.* refetches automatically whenever a server function calls invalidateTags. Every browser tab. Every active component. No invalidateQueries needed on the client.
Next step
Redis for multi-instance, scope isolation, SPA + backend setup, and all framework adapters.
Read the guide →See it live
Live examples with real SSE — tagged queries, group invalidation, multi-tab sync, and more.
Explore demos →