Skip to content
Development
Command

/tanstack-query

Check for TanStack Query pattern violations (query keys, optimistic updates, cache seeding)

From plugin
pane
36435 skills6 agents35 commands
Install
$ npx -y skills add dcouple/Pane --agent claude-code

How it fires

How this command gets triggered: by you, by Claude, or both.

  • Fires itselfClaude auto-loads it when your prompt matches the work.
  • You can call itInvoke it directly when you want it.
  • Slash command/tanstack-query

Context preview

What this command does when you run it.

Check for TanStack Query pattern violations (query keys, optimistic updates, cache seeding)

Command definition

tanstack-query.md
allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git show:*), Bash(git rev-parse:*), Bash(git branch:*), Read, Grep, Glob, TodoWrite
description: Check for TanStack Query pattern violations (query keys, optimistic updates, cache seeding)

/review:tanstack-query - TanStack Query Patterns Checker

You are reviewing code changes for violations of **TanStack Query patterns** established in this codebase.

**Canonical reference:** `tmp/research/tanstack-query-guidelines.md` — read this first for full context.

The Principles

1. Global Configuration — No Per-Query Overrides

The global `QueryClient` defaults handle freshness. Do NOT add per-query overrides.

**Global defaults (in `providers/QueryProvider.tsx`):**

  • `staleTime: 0` — always stale, always refetch on mount/focus/reconnect
  • `refetchOnWindowFocus: true`
  • `refetchOnReconnect: true`
  • `retry: 3` with exponential backoff

**Rules:**

  • Do NOT add per-query `staleTime` unless data is truly immutable (write-once, content-addressed). The only valid constant is `STALE_TIME.IMMUTABLE` (`Infinity`).
  • Do NOT add per-query `refetchOnWindowFocus` or `refetchOnReconnect` — global defaults apply.
  • Do NOT add `retry` overrides unless the query has special failure semantics.

2. Domain-Specific Hierarchical Query Key Factories

Every domain gets its own `<domain>Keys.ts` factory with hierarchical, self-referential keys. Never use raw string arrays. Never add domain keys to the flat `qk` object.

**Gold standard exemplars:**

  • `apps/webapp/src/hooks/feed/feedKeys.ts`
  • `apps/webapp/src/hooks/conversations/conversationKeys.ts`
  • `apps/webapp/src/hooks/todos/todoKeys.ts`

**Required structure:**

export const domainKeys = {
  all: ['domain'] as const,
  lists: (workspaceId, params?) => [...domainKeys.all, 'list', workspaceId ?? null, normalizeParams(params)] as const,
  detail: (id) => [...domainKeys.all, 'detail', id ?? null] as const,
};

**Key properties:**

  • Self-referential: all keys spread from `...domainKeys.all`
  • Param normalization: dedicated function, `undefined` → `null`, sorted arrays, Dates → ISO strings
  • `as const` on all keys for type inference
  • Co-located with hooks in `src/hooks/<domain>/`

3. Cache Invalidation — Predicate Helpers for Parameterized Lists

TanStack Query's `invalidateQueries({ queryKey })` uses **prefix matching**. Calling a factory with no params still produces a full key with trailing defaults, which is **narrower** than the base prefix.

**The trap:**

// BAD: Factory call includes default params, misses other param variants
queryClient.invalidateQueries({ queryKey: conversationKeys.lists(workspaceId) });
// Produces ['conversations', 'list', workspaceId, {}] — misses { includeMessages: true }

// GOOD: Use the predicate helper
invalidateWorkspaceConversations(queryClient, workspaceId);

**Rule:** Any domain with parameterized list queries must have an invalidation helper (`<domain>Invalidation.ts`) that uses a predicate to match all variants. Use the predicate helper in all mutations, not raw factory calls.

**When to use what:**

| Scenario | Approach | |----------|----------| | All list variants for a workspace | Predicate helper | | Specific detail query | Exact key: `domainKeys.detail(id)` | | Everything in a domain | Base prefix: `domainKeys.all` | | Simple key with no params | Factory call is fine | | All variants of a parameterized parent | Prefix only, no trailing params: `['workspaces']` |

4. Optimistic Updates via `onMutate`

All optimistic state changes happen inside `useMutation`'s `onMutate` with snapshot + rollback. Never mutate cache from component handlers.

**Exemplar:** `apps/webapp/src/hooks/todos/useTodo.ts` (`useUpdateTodoTitle`)

**Required 6-step pattern:** 1. `cancelQueries` — prevent race conditions 2. Snapshot current state 3. Optimistically update cache 4. Return context for rollback 5. `onError` — restore from snapshot 6. `onSettled` — invalidate to ensure server truth

**When NOT to use optimistic updates:**

  • Server response determines new state (AI generation, complex transforms) → use `onSuccess` invalidation
  • Destructive operations (delete) → prefer invalidation + loading state

**`setQueryData` in `onSuccess` is acceptable** for caching server responses to detail queries (this is NOT an optimistic update).

5. Instant Loading via `initialData` Cache Seeding

List-to-detail navigation uses `initialData` to pull from list cache. Eliminates loading spinners.

**Exemplars:**

  • `apps/webapp/src/hooks/todos/useTodo.ts` (`findTodoInCache` + `useTodoDetail`)
  • `apps/webapp/src/hooks/feed/useFeedItem.ts` (`findFeedItemInCache`)
  • `apps/webapp/src/hooks/conversations/useConversation.ts` (`findConversationInCache`)

**Required pattern:**

  • `findXInCache` helper searches `domainKeys.all` across all query shapes
  • Handles both flat and infinite query structures (iterate `data.pages` for infinite queries)
  • Used as `initialData: () => findXInCache(queryClient, id)`
  • Use `initialData` not `placeholderData` — `initialData` caches and counts as "real" data

6. WebSocket Cache Updates

WebSocket handlers can update cache directly via `setQueryData` — this is server-sourced data, NOT an optimistic update. Always pair with debounced invalidation.

**Rule:** On WebSocket reconnect, invalidate all workspace caches using predicate-based helpers (not specific key variants).

7. `qk` vs Domain Factories

The flat `qk` object is for simple, non-parameterized keys. Anything with parameterized lists, multiple query shapes, or invalidation complexity gets a domain factory.

**Move a key from `qk` to a domain factory when:** 1. It has 3+ query key usages 2. Lists have filter/sort/pagination params 3. You need predicate-based invalidation to match all variants 4. There's a list → detail navigation pattern (cache seeding)

8. ESLint Suppressions

The `@tanstack/query/exhaustive-deps` rule requires all `queryFn` variabl

Read more
Ships withpane

pnpm dlx runpane@latest

Get the whole plugin