Domphy

Query Invalidation

What is invalidation?

Marking a query as "invalid" tells @domphy/query that the cached data is stale and should be refetched the next time it is observed. Invalidated queries refetch immediately if they have active subscribers; otherwise they refetch lazily on next mount.

invalidateQueries

The main API — invalidates matching queries:

import { QueryClient } from "@domphy/query"

const queryClient = new QueryClient()

// Invalidate ALL queries with the "posts" prefix
await queryClient.invalidateQueries({ queryKey: ["posts"] })

This fuzzy-matches by default — ["posts"] invalidates ["posts"], ["posts", 1], ["posts", "list"], etc.

Exact match

Only invalidate a specific key:

// Invalidate ONLY ["posts", 1] — not ["posts"] or ["posts", 2]
await queryClient.invalidateQueries({
  queryKey: ["posts", 1],
  exact: true,
})

Invalidate after mutation

The most common pattern — after a write, invalidate the affected queries:

import { createMutation } from "@domphy/query/domphy"

const createPost = createMutation(queryClient, {
  mutationFn: (data: PostInput) => api.post("/posts", data),
  onSuccess: async () => {
    // Invalidate the list — it will refetch with the new post included
    await queryClient.invalidateQueries({ queryKey: ["posts"] })
  },
})

const updatePost = createMutation(queryClient, {
  mutationFn: ({ id, data }: { id: number; data: Partial<Post> }) => api.patch(`/posts/${id}`, data),
  onSuccess: (_result, variables) => {
    // Invalidate the specific post AND the list
    queryClient.invalidateQueries({ queryKey: ["posts", variables.id] })
    queryClient.invalidateQueries({ queryKey: ["posts"] })
  },
})

Predicate-based invalidation

For complex matching logic, use a predicate function:

// Invalidate all post-related queries for a specific author
await queryClient.invalidateQueries({
  predicate: (query) => {
    const key = query.queryKey as string[]
    return key[0] === "posts" && key.includes(authorId)
  },
})

// Invalidate all queries that have an active observer and are older than 1 minute
await queryClient.invalidateQueries({
  predicate: (query) => {
    const age = Date.now() - (query.state.dataUpdatedAt ?? 0)
    return age > 60_000 && query.getObserversCount() > 0
  },
})

refetchType

Control what happens to invalidated queries:

await queryClient.invalidateQueries({
  queryKey: ["posts"],
  refetchType: "active",   // only refetch queries with active observers (default)
})

await queryClient.invalidateQueries({
  queryKey: ["posts"],
  refetchType: "all",      // refetch even inactive (background) queries
})

await queryClient.invalidateQueries({
  queryKey: ["posts"],
  refetchType: "none",     // mark stale but don't refetch (refetch on next mount)
})

Optimistic updates + invalidation

Combine optimistic cache updates with post-mutation invalidation for smooth UX:

interface Todo { id: string; text: string; done: boolean }

const toggleTodo = createMutation(queryClient, {
  mutationFn: (id: string) => api.patch(`/todos/${id}/toggle`),

  onMutate: async (id) => {
    await queryClient.cancelQueries({ queryKey: ["todos"] })
    const previous = queryClient.getQueryData<Todo[]>(["todos"])

    // Optimistic update
    queryClient.setQueryData<Todo[]>(["todos"], (todos = []) =>
      todos.map((t) => t.id === id ? { ...t, done: !t.done } : t)
    )

    return { previous }
  },

  onError: (_err, _id, context) => {
    queryClient.setQueryData(["todos"], context?.previous)
  },

  onSettled: () => {
    // Final source-of-truth sync regardless of success/failure
    queryClient.invalidateQueries({ queryKey: ["todos"] })
  },
})

removeQueries

Remove a query from the cache entirely (vs. just marking it stale):

// Remove when user logs out
function logout() {
  // Remove all private data from the cache
  queryClient.removeQueries({ queryKey: ["user"] })
  queryClient.removeQueries({ queryKey: ["notifications"] })
  queryClient.removeQueries({ queryKey: ["orders"] })
}

resetQueries

Reset a query back to its initial state (removes data, sets status to "pending"):

// Reset a form query when the user clicks "clear"
queryClient.resetQueries({ queryKey: ["draft", formId] })

refetchQueries

Force a refetch without marking as invalid first:

// Pull-to-refresh
async function onPullToRefresh() {
  await queryClient.refetchQueries({
    queryKey: ["feed"],
    type: "active",
  })
}

Tracking invalidations reactively

Listen for cache events to react to invalidations:

import { toState } from "@domphy/core"

const isRefreshing = toState(false)

queryClient.getQueryCache().subscribe((event) => {
  if (event?.type === "updated" && event.action.type === "fetch") {
    isRefreshing.set(queryClient.isFetching() > 0)
  }
})