Edit

@telefunc/tanstack-query

BetaTelefunc Stream is in beta: breaking changes may occur in any version update.

Live queries for TanStack Query — invalidate a query key on the server, and every connected client with a matching query refetches automatically.

The @telefunc/tanstack-query integration is powered by Telefunc Stream.

Install

npm install @telefunc/tanstack-query
pnpm add @telefunc/tanstack-query
bun add @telefunc/tanstack-query
yarn add @telefunc/tanstack-query

Wrap your QueryClient with withTelefunc():

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { withTelefunc } from '@telefunc/tanstack-query'
 
const queryClient = withTelefunc(new QueryClient())
 
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyApp />
    </QueryClientProvider>
  )
}

withTelefunc() returns the same new QueryClient() instance:

  • All options and APIs continue to work as before
  • Any TanStack Query adapter works: React, Vue, Svelte, Solid

There isn't any other setup: Telefunc finds @telefunc/* packages in your package.json and auto-loads them.

Local vs global keys

After installing @telefunc/tanstack-query, keys prefixed with global: become special: they invalidate globally — every connected client with a matching query refetches.

All other keys remain normal (invalidate locally, i.e. current tab only).

How does it work? Under the hood, the global: key is sent to the server and then broadcast to every client using a query key that matches. Learn more at How it works.

queryKey: ['todos']                          // local — current tab only
queryKey: ['global:todos', `user:${userId}`] // global — all clients with a matching query refetch
queryKey: ['global:documents', docId]        // global

A key is global when its first element is a string starting with global:, for example:

  • ['global:todos', `user:${userId}`] is global
  • ['todos', 'global:x'] is local

Mutations

meta.invalidates

Use meta.invalidates on mutations to invalidate matching queries after the mutation succeeds.

const { data: todos } = useQuery({
  queryKey: ['todos'],
  queryFn: () => getTodos()
})
 
const add = useMutation({
  mutationFn: (text) => addTodo(text),
  meta: { invalidates: [['todos']] }
})
const { data: todos } = useQuery({
  queryKey: ['todos'],
  queryFn: () => getTodos(),
})
 
const add = useMutation({
  mutationFn: (text: string) => addTodo(text),
  meta: { invalidates: [['todos']] },
})

meta.invalidates is a @telefunc/tanstack-query convention: TanStack Query itself attaches no behavior to meta.

Global keys

When using a global key, every connected client refetches.

Local and global keys can be mixed in a single mutation:

meta: {
  invalidates: [
    ['my-drafts'],        // local — this client only
    ['global:documents'], // global — every connected client
  ],
}

Example:

Consider a collaborative document editor: when one user edits a document, every other user viewing it refetches:

const { data: doc } = useQuery({
  queryKey: ['global:documents', docId],
  queryFn: () => getDocument(docId)
})
 
const edit = useMutation({
  mutationFn: (content) => updateDocument(docId, content),
  meta: { invalidates: [['global:documents', docId]] }
})
const { data: doc } = useQuery({
  queryKey: ['global:documents', docId],
  queryFn: () => getDocument(docId),
})
 
const edit = useMutation({
  mutationFn: (content: string) => updateDocument(docId, content),
  meta: { invalidates: [['global:documents', docId]] },
})

Two rules

Make sure that:

  1. queryFn and mutationFn must call a telefunction.
  2. Return the telefunction call directly. Transform the result with select instead of inside queryFn:
    // ❌ Broken: queryFn doesn't directly return getTodos()
    queryFn: async () => (await getTodos()).items
     
    // ✅ Return the call directly, transform with select
    queryFn: () => getTodos(),
    select: (todos) => todos.items,

    The @telefunc/tanstack-query integration must access the return value of the telefunction call.

Server-side invalidation

For changes not triggered by a client mutation (e.g. background jobs, webhooks), you can use invalidate():

// Environment: server
 
import { invalidate } from '@telefunc/tanstack-query/server'
 
// e.g. a CMS publishes new content
invalidate(['global:articles'])
 
// a specific document was updated
invalidate(['global:documents', docId])

invalidate() is for global keys only. (Local keys have no cross-client subscribers, so they wouldn't reach any client.)

Invalidation is prefix-based: invalidating ['global:documents'] matches ['global:documents', docId] too. This is the same behavior as TanStack Query's invalidateQueries().

How it works

Local keys

Query: normal (@telefunc/tanstack-query has no effect).

Mutation:

  1. The mutation succeeds.
  2. withTelefunc() calls invalidateQueries() on the current client for the matching local keys.

Global keys

Global keys are powered by Telefunc Stream under the hood (in particular channels and broadcasts).

Query:

  1. If it's a global key, then withTelefunc() sends queryKey alongside the query telefunction call.
  2. The query telefunction executes, and the client subscribes to invalidation events for that key.

    It subscribes only if the telefunction call succeeds (i.e. it doesn't throw an error), therefore authorization is respected.

Mutation:

  1. withTelefunc() sends all the global keys from meta.invalidates alongside the telefunction mutation call.
  2. The server broadcasts an invalidation event to the clients subscribed to a query key that matches a global key.
  3. Every connected client (with a subscription that matches a global key) calls invalidateQueries().

See also