/tanstack-query
Check for TanStack Query pattern violations (query keys, optimistic updates, cache seeding)
$ npx -y skills add dcouple/Pane --agent claude-codeHow 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.mdallowed-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
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
Repo: dcouple/Pane
Other commands on pane.
- /commit
Create git commits with user approval and no Claude attribution
Open command - /create_plan
You are tasked with creating detailed implementation plans through an interactive, iterative process. You should be skeptical, thorough, and work collaboratively with the user to produce high-quality technical specifications.
Open command - /describe_pr
Generate comprehensive PR descriptions following repository templates
Open command - /implement_plan
You are tasked with implementing an approved technical plan from `thoughts/shared/plans/`. These plans contain phases with specific changes and success criteria.
Open command - /iterate_plan
Iterate on existing implementation plans with thorough research and updates
Open command - /research_codebase
You are tasked with conducting comprehensive research across the codebase to answer user questions. You will spawn one or more parallel sub-agents to perform your reserach.
Open command

