@smicolon/ai-kit 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -40
- package/dist/index.js +260 -126
- package/package.json +5 -5
- package/.claude-plugin/marketplace.json +0 -369
- package/packs/architect/CHANGELOG.md +0 -17
- package/packs/architect/README.md +0 -58
- package/packs/architect/agents/system-architect.md +0 -768
- package/packs/architect/commands/diagram-create.md +0 -300
- package/packs/better-auth/.mcp.json +0 -14
- package/packs/better-auth/CHANGELOG.md +0 -26
- package/packs/better-auth/README.md +0 -125
- package/packs/better-auth/agents/auth-architect.md +0 -278
- package/packs/better-auth/commands/auth-provider-add.md +0 -265
- package/packs/better-auth/commands/auth-setup.md +0 -298
- package/packs/better-auth/skills/auth-security/SKILL.md +0 -425
- package/packs/better-auth/skills/better-auth-patterns/SKILL.md +0 -455
- package/packs/dev-loop/CHANGELOG.md +0 -69
- package/packs/dev-loop/README.md +0 -155
- package/packs/dev-loop/commands/cancel-dev.md +0 -21
- package/packs/dev-loop/commands/dev-loop.md +0 -72
- package/packs/dev-loop/commands/dev-plan.md +0 -351
- package/packs/dev-loop/hooks/hooks.json +0 -15
- package/packs/dev-loop/hooks/stop-hook.sh +0 -178
- package/packs/dev-loop/scripts/setup-dev-loop.sh +0 -194
- package/packs/dev-loop/skills/tdd-planner/SKILL.md +0 -249
- package/packs/dev-loop/skills/tdd-planner/references/framework-patterns.md +0 -874
- package/packs/dev-loop/skills/tdd-planner/references/good-example.md +0 -260
- package/packs/dev-loop/skills/tdd-planner/references/plan-template.md +0 -275
- package/packs/django/CHANGELOG.md +0 -39
- package/packs/django/README.md +0 -92
- package/packs/django/agents/django-architect.md +0 -182
- package/packs/django/agents/django-builder.md +0 -250
- package/packs/django/agents/django-feature-based.md +0 -420
- package/packs/django/agents/django-reviewer.md +0 -253
- package/packs/django/agents/django-tester.md +0 -230
- package/packs/django/commands/api-endpoint.md +0 -285
- package/packs/django/commands/model-create.md +0 -178
- package/packs/django/commands/test-generate.md +0 -325
- package/packs/django/rules/migrations.md +0 -138
- package/packs/django/rules/models.md +0 -167
- package/packs/django/rules/serializers.md +0 -126
- package/packs/django/rules/services.md +0 -131
- package/packs/django/rules/tests.md +0 -140
- package/packs/django/rules/views.md +0 -102
- package/packs/django/skills/import-convention-enforcer/SKILL.md +0 -226
- package/packs/django/skills/import-convention-enforcer/patterns/django-imports.md +0 -343
- package/packs/django/skills/migration-safety-checker/SKILL.md +0 -375
- package/packs/django/skills/model-entity-validator/SKILL.md +0 -298
- package/packs/django/skills/performance-optimizer/SKILL.md +0 -447
- package/packs/django/skills/red-phase-verifier/SKILL.md +0 -180
- package/packs/django/skills/security-first-validator/SKILL.md +0 -435
- package/packs/django/skills/test-coverage-advisor/SKILL.md +0 -394
- package/packs/django/skills/test-validity-checker/SKILL.md +0 -194
- package/packs/failure-log/CHANGELOG.md +0 -20
- package/packs/failure-log/README.md +0 -168
- package/packs/failure-log/commands/failure-add.md +0 -106
- package/packs/failure-log/commands/failure-list.md +0 -89
- package/packs/failure-log/hooks/hooks.json +0 -16
- package/packs/failure-log/hooks/scripts/inject-failures.sh +0 -64
- package/packs/failure-log/skills/failure-log-manager/SKILL.md +0 -164
- package/packs/flutter/CHANGELOG.md +0 -19
- package/packs/flutter/README.md +0 -170
- package/packs/flutter/agents/flutter-architect.md +0 -166
- package/packs/flutter/agents/flutter-builder.md +0 -303
- package/packs/flutter/agents/release-manager.md +0 -355
- package/packs/flutter/commands/fastlane-setup.md +0 -188
- package/packs/flutter/commands/flutter-build.md +0 -90
- package/packs/flutter/commands/flutter-deploy.md +0 -133
- package/packs/flutter/commands/flutter-test.md +0 -117
- package/packs/flutter/commands/signing-setup.md +0 -209
- package/packs/flutter/hooks/hooks.json +0 -17
- package/packs/flutter/skills/fastlane-knowledge/SKILL.md +0 -193
- package/packs/flutter/skills/flutter-architecture/SKILL.md +0 -127
- package/packs/flutter/skills/store-publishing/SKILL.md +0 -163
- package/packs/hono/CHANGELOG.md +0 -19
- package/packs/hono/README.md +0 -143
- package/packs/hono/agents/hono-architect.md +0 -240
- package/packs/hono/agents/hono-builder.md +0 -285
- package/packs/hono/agents/hono-reviewer.md +0 -279
- package/packs/hono/agents/hono-tester.md +0 -346
- package/packs/hono/commands/middleware-create.md +0 -223
- package/packs/hono/commands/project-init.md +0 -306
- package/packs/hono/commands/route-create.md +0 -153
- package/packs/hono/commands/rpc-client.md +0 -263
- package/packs/hono/skills/cloudflare-bindings/SKILL.md +0 -408
- package/packs/hono/skills/hono-patterns/SKILL.md +0 -309
- package/packs/hono/skills/rpc-typesafe/SKILL.md +0 -388
- package/packs/hono/skills/zod-validation/SKILL.md +0 -332
- package/packs/nestjs/CHANGELOG.md +0 -29
- package/packs/nestjs/README.md +0 -75
- package/packs/nestjs/agents/nestjs-architect.md +0 -402
- package/packs/nestjs/agents/nestjs-builder.md +0 -301
- package/packs/nestjs/agents/nestjs-tester.md +0 -437
- package/packs/nestjs/commands/module-create.md +0 -369
- package/packs/nestjs/rules/controllers.md +0 -92
- package/packs/nestjs/rules/dto.md +0 -124
- package/packs/nestjs/rules/entities.md +0 -102
- package/packs/nestjs/rules/services.md +0 -106
- package/packs/nestjs/skills/barrel-export-manager/SKILL.md +0 -389
- package/packs/nestjs/skills/import-convention-enforcer/SKILL.md +0 -365
- package/packs/nextjs/CHANGELOG.md +0 -36
- package/packs/nextjs/README.md +0 -76
- package/packs/nextjs/agents/frontend-tester.md +0 -680
- package/packs/nextjs/agents/frontend-visual.md +0 -820
- package/packs/nextjs/agents/nextjs-architect.md +0 -331
- package/packs/nextjs/agents/nextjs-modular.md +0 -433
- package/packs/nextjs/commands/component-create.md +0 -398
- package/packs/nextjs/rules/api-routes.md +0 -129
- package/packs/nextjs/rules/components.md +0 -106
- package/packs/nextjs/rules/hooks.md +0 -132
- package/packs/nextjs/skills/accessibility-validator/SKILL.md +0 -445
- package/packs/nextjs/skills/import-convention-enforcer/SKILL.md +0 -399
- package/packs/nextjs/skills/react-form-validator/SKILL.md +0 -569
- package/packs/nuxtjs/CHANGELOG.md +0 -30
- package/packs/nuxtjs/README.md +0 -56
- package/packs/nuxtjs/agents/frontend-tester.md +0 -680
- package/packs/nuxtjs/agents/frontend-visual.md +0 -820
- package/packs/nuxtjs/agents/nuxtjs-architect.md +0 -537
- package/packs/nuxtjs/commands/component-create.md +0 -223
- package/packs/nuxtjs/rules/components.md +0 -101
- package/packs/nuxtjs/rules/composables.md +0 -118
- package/packs/nuxtjs/rules/server-routes.md +0 -127
- package/packs/nuxtjs/skills/accessibility-validator/SKILL.md +0 -183
- package/packs/nuxtjs/skills/import-convention-enforcer/SKILL.md +0 -196
- package/packs/nuxtjs/skills/veevalidate-form-validator/SKILL.md +0 -190
- package/packs/onboard/CHANGELOG.md +0 -22
- package/packs/onboard/README.md +0 -103
- package/packs/onboard/agents/onboard-guide.md +0 -118
- package/packs/onboard/commands/onboard.md +0 -313
- package/packs/onboard/skills/onboard-context-provider/SKILL.md +0 -98
- package/packs/tanstack-router/CHANGELOG.md +0 -30
- package/packs/tanstack-router/README.md +0 -113
- package/packs/tanstack-router/agents/tanstack-architect.md +0 -173
- package/packs/tanstack-router/agents/tanstack-builder.md +0 -360
- package/packs/tanstack-router/agents/tanstack-tester.md +0 -454
- package/packs/tanstack-router/commands/form-create.md +0 -313
- package/packs/tanstack-router/commands/query-create.md +0 -263
- package/packs/tanstack-router/commands/route-create.md +0 -190
- package/packs/tanstack-router/commands/table-create.md +0 -413
- package/packs/tanstack-router/skills/ai-patterns/SKILL.md +0 -370
- package/packs/tanstack-router/skills/db-patterns/SKILL.md +0 -346
- package/packs/tanstack-router/skills/devtools-patterns/SKILL.md +0 -415
- package/packs/tanstack-router/skills/form-patterns/SKILL.md +0 -425
- package/packs/tanstack-router/skills/pacer-patterns/SKILL.md +0 -341
- package/packs/tanstack-router/skills/query-patterns/SKILL.md +0 -359
- package/packs/tanstack-router/skills/router-patterns/SKILL.md +0 -285
- package/packs/tanstack-router/skills/store-patterns/SKILL.md +0 -351
- package/packs/tanstack-router/skills/table-patterns/SKILL.md +0 -531
- package/packs/tanstack-router/skills/tanstack-conventions/SKILL.md +0 -428
- package/packs/tanstack-router/skills/virtual-patterns/SKILL.md +0 -490
- package/packs/worktree/CHANGELOG.md +0 -45
- package/packs/worktree/README.md +0 -219
- package/packs/worktree/commands/wt.md +0 -93
- package/packs/worktree/scripts/wt.sh +0 -957
- package/packs/worktree/skills/worktree-manager/SKILL.md +0 -113
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: TanStack Pacer Patterns (Beta)
|
|
3
|
-
description: >-
|
|
4
|
-
TanStack Pacer patterns for rate limiting, debouncing, and throttling.
|
|
5
|
-
Activates when implementing search inputs, API rate limiting, or
|
|
6
|
-
performance optimization. NOTE: Beta library - API may change.
|
|
7
|
-
version: 1.0.0
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# TanStack Pacer Patterns (Beta)
|
|
11
|
-
|
|
12
|
-
> **Beta Library**: TanStack Pacer is in beta. APIs may change between versions.
|
|
13
|
-
|
|
14
|
-
TanStack Pacer provides utilities for rate limiting, debouncing, throttling, and async queuing.
|
|
15
|
-
|
|
16
|
-
## Debounce
|
|
17
|
-
|
|
18
|
-
Delay execution until input stops for a specified time.
|
|
19
|
-
|
|
20
|
-
### Basic Debounce
|
|
21
|
-
```typescript
|
|
22
|
-
import { debounce } from '@tanstack/pacer'
|
|
23
|
-
|
|
24
|
-
const debouncedSearch = debounce((query: string) => {
|
|
25
|
-
console.log('Searching:', query)
|
|
26
|
-
return fetch(`/api/search?q=${query}`)
|
|
27
|
-
}, 300)
|
|
28
|
-
|
|
29
|
-
// Only executes after 300ms of no calls
|
|
30
|
-
debouncedSearch('h')
|
|
31
|
-
debouncedSearch('he')
|
|
32
|
-
debouncedSearch('hel')
|
|
33
|
-
debouncedSearch('hell')
|
|
34
|
-
debouncedSearch('hello') // Only this executes
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Debounce in React
|
|
38
|
-
```typescript
|
|
39
|
-
import { useDebounce } from '@tanstack/pacer-react'
|
|
40
|
-
import { useState } from 'react'
|
|
41
|
-
|
|
42
|
-
function SearchInput() {
|
|
43
|
-
const [query, setQuery] = useState('')
|
|
44
|
-
|
|
45
|
-
const debouncedQuery = useDebounce(query, 300)
|
|
46
|
-
|
|
47
|
-
// Use debouncedQuery for API calls
|
|
48
|
-
const { data } = useQuery({
|
|
49
|
-
queryKey: ['search', debouncedQuery],
|
|
50
|
-
queryFn: () => searchApi.search(debouncedQuery),
|
|
51
|
-
enabled: debouncedQuery.length > 0,
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<div>
|
|
56
|
-
<input
|
|
57
|
-
value={query}
|
|
58
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
59
|
-
placeholder="Search..."
|
|
60
|
-
/>
|
|
61
|
-
{data && <SearchResults results={data} />}
|
|
62
|
-
</div>
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Debounced Callback
|
|
68
|
-
```typescript
|
|
69
|
-
import { useDebouncedCallback } from '@tanstack/pacer-react'
|
|
70
|
-
|
|
71
|
-
function AutoSaveEditor({ postId }: { postId: string }) {
|
|
72
|
-
const [content, setContent] = useState('')
|
|
73
|
-
const updatePost = useUpdatePost()
|
|
74
|
-
|
|
75
|
-
const saveContent = useDebouncedCallback(
|
|
76
|
-
async (content: string) => {
|
|
77
|
-
await updatePost.mutateAsync({ id: postId, content })
|
|
78
|
-
},
|
|
79
|
-
1000, // Save after 1s of no typing
|
|
80
|
-
{ maxWait: 5000 } // But at least every 5s
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
84
|
-
const newContent = e.target.value
|
|
85
|
-
setContent(newContent)
|
|
86
|
-
saveContent(newContent)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<textarea value={content} onChange={handleChange} />
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Throttle
|
|
96
|
-
|
|
97
|
-
Limit execution to at most once per interval.
|
|
98
|
-
|
|
99
|
-
### Basic Throttle
|
|
100
|
-
```typescript
|
|
101
|
-
import { throttle } from '@tanstack/pacer'
|
|
102
|
-
|
|
103
|
-
const throttledScroll = throttle((position: number) => {
|
|
104
|
-
console.log('Scroll position:', position)
|
|
105
|
-
}, 100)
|
|
106
|
-
|
|
107
|
-
// Executes at most once every 100ms
|
|
108
|
-
window.addEventListener('scroll', () => {
|
|
109
|
-
throttledScroll(window.scrollY)
|
|
110
|
-
})
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### Throttle in React
|
|
114
|
-
```typescript
|
|
115
|
-
import { useThrottledCallback } from '@tanstack/pacer-react'
|
|
116
|
-
import { useEffect } from 'react'
|
|
117
|
-
|
|
118
|
-
function ScrollTracker() {
|
|
119
|
-
const trackScroll = useThrottledCallback(
|
|
120
|
-
(position: number) => {
|
|
121
|
-
analytics.track('scroll', { position })
|
|
122
|
-
},
|
|
123
|
-
500
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
useEffect(() => {
|
|
127
|
-
const handleScroll = () => trackScroll(window.scrollY)
|
|
128
|
-
window.addEventListener('scroll', handleScroll)
|
|
129
|
-
return () => window.removeEventListener('scroll', handleScroll)
|
|
130
|
-
}, [trackScroll])
|
|
131
|
-
|
|
132
|
-
return null
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### Throttled API Calls
|
|
137
|
-
```typescript
|
|
138
|
-
import { useThrottledCallback } from '@tanstack/pacer-react'
|
|
139
|
-
|
|
140
|
-
function LiveSearch() {
|
|
141
|
-
const [results, setResults] = useState<Result[]>([])
|
|
142
|
-
|
|
143
|
-
const search = useThrottledCallback(
|
|
144
|
-
async (query: string) => {
|
|
145
|
-
const data = await searchApi.search(query)
|
|
146
|
-
setResults(data)
|
|
147
|
-
},
|
|
148
|
-
200, // At most 5 requests per second
|
|
149
|
-
{ leading: true, trailing: true }
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<input
|
|
154
|
-
onChange={(e) => search(e.target.value)}
|
|
155
|
-
placeholder="Search..."
|
|
156
|
-
/>
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
## Rate Limiting
|
|
162
|
-
|
|
163
|
-
Control the rate of operations.
|
|
164
|
-
|
|
165
|
-
### Rate Limiter
|
|
166
|
-
```typescript
|
|
167
|
-
import { createRateLimiter } from '@tanstack/pacer'
|
|
168
|
-
|
|
169
|
-
const apiLimiter = createRateLimiter({
|
|
170
|
-
limit: 10, // 10 requests
|
|
171
|
-
interval: 1000, // per second
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
async function fetchData(id: string) {
|
|
175
|
-
await apiLimiter.acquire() // Wait if rate limited
|
|
176
|
-
return fetch(`/api/data/${id}`)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Safe to call rapidly - will be rate limited
|
|
180
|
-
await Promise.all(
|
|
181
|
-
ids.map(id => fetchData(id))
|
|
182
|
-
)
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
### Per-Key Rate Limiting
|
|
186
|
-
```typescript
|
|
187
|
-
import { createKeyedRateLimiter } from '@tanstack/pacer'
|
|
188
|
-
|
|
189
|
-
const userLimiter = createKeyedRateLimiter({
|
|
190
|
-
limit: 5,
|
|
191
|
-
interval: 60000, // 5 requests per minute per user
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
async function handleUserAction(userId: string, action: Action) {
|
|
195
|
-
const limiter = userLimiter.get(userId)
|
|
196
|
-
if (!limiter.tryAcquire()) {
|
|
197
|
-
throw new Error('Rate limited. Please try again later.')
|
|
198
|
-
}
|
|
199
|
-
return processAction(action)
|
|
200
|
-
}
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
## Async Queue
|
|
204
|
-
|
|
205
|
-
Process async operations sequentially or with concurrency limits.
|
|
206
|
-
|
|
207
|
-
### Sequential Queue
|
|
208
|
-
```typescript
|
|
209
|
-
import { createAsyncQueue } from '@tanstack/pacer'
|
|
210
|
-
|
|
211
|
-
const uploadQueue = createAsyncQueue({
|
|
212
|
-
concurrency: 1, // One at a time
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
async function uploadFiles(files: File[]) {
|
|
216
|
-
const results = await Promise.all(
|
|
217
|
-
files.map(file =>
|
|
218
|
-
uploadQueue.add(() => uploadFile(file))
|
|
219
|
-
)
|
|
220
|
-
)
|
|
221
|
-
return results
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Concurrent Queue with Limit
|
|
226
|
-
```typescript
|
|
227
|
-
import { createAsyncQueue } from '@tanstack/pacer'
|
|
228
|
-
|
|
229
|
-
const processQueue = createAsyncQueue({
|
|
230
|
-
concurrency: 3, // Max 3 concurrent
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
function ProcessingStatus() {
|
|
234
|
-
const { pending, active, completed } = processQueue.status
|
|
235
|
-
|
|
236
|
-
return (
|
|
237
|
-
<div>
|
|
238
|
-
<span>Pending: {pending}</span>
|
|
239
|
-
<span>Active: {active}</span>
|
|
240
|
-
<span>Completed: {completed}</span>
|
|
241
|
-
</div>
|
|
242
|
-
)
|
|
243
|
-
}
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### Queue with Priority
|
|
247
|
-
```typescript
|
|
248
|
-
import { createAsyncQueue } from '@tanstack/pacer'
|
|
249
|
-
|
|
250
|
-
const taskQueue = createAsyncQueue({
|
|
251
|
-
concurrency: 2,
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
// Higher priority tasks run first
|
|
255
|
-
taskQueue.add(() => processTask('low'), { priority: 1 })
|
|
256
|
-
taskQueue.add(() => processTask('high'), { priority: 10 })
|
|
257
|
-
taskQueue.add(() => processTask('urgent'), { priority: 100 })
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
## Integration with TanStack Query
|
|
261
|
-
|
|
262
|
-
```typescript
|
|
263
|
-
import { useDebounce } from '@tanstack/pacer-react'
|
|
264
|
-
import { useQuery } from '@tanstack/react-query'
|
|
265
|
-
|
|
266
|
-
function SearchWithDebounce() {
|
|
267
|
-
const [input, setInput] = useState('')
|
|
268
|
-
const debouncedInput = useDebounce(input, 300)
|
|
269
|
-
|
|
270
|
-
const { data, isLoading } = useQuery({
|
|
271
|
-
queryKey: ['search', debouncedInput],
|
|
272
|
-
queryFn: () => searchApi.search(debouncedInput),
|
|
273
|
-
enabled: debouncedInput.length >= 2,
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
return (
|
|
277
|
-
<div>
|
|
278
|
-
<input
|
|
279
|
-
value={input}
|
|
280
|
-
onChange={(e) => setInput(e.target.value)}
|
|
281
|
-
placeholder="Search (min 2 chars)..."
|
|
282
|
-
/>
|
|
283
|
-
{isLoading && <Spinner />}
|
|
284
|
-
{data && <Results items={data} />}
|
|
285
|
-
</div>
|
|
286
|
-
)
|
|
287
|
-
}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
## Common Use Cases
|
|
291
|
-
|
|
292
|
-
| Scenario | Solution |
|
|
293
|
-
|----------|----------|
|
|
294
|
-
| Search input | Debounce 300ms |
|
|
295
|
-
| Auto-save | Debounce 1s with maxWait |
|
|
296
|
-
| Scroll tracking | Throttle 100-200ms |
|
|
297
|
-
| API rate limits | Rate limiter |
|
|
298
|
-
| File uploads | Async queue with concurrency |
|
|
299
|
-
| Resize handler | Throttle 100ms |
|
|
300
|
-
|
|
301
|
-
## Conventions
|
|
302
|
-
|
|
303
|
-
1. **Debounce for inputs** - Always debounce search/filter inputs
|
|
304
|
-
2. **Throttle for events** - Use for scroll, resize, mousemove
|
|
305
|
-
3. **Rate limit APIs** - Protect against abuse
|
|
306
|
-
4. **Queue heavy operations** - Use for file uploads, bulk operations
|
|
307
|
-
5. **Cleanup** - Cancel pending operations on unmount
|
|
308
|
-
|
|
309
|
-
## Anti-Patterns
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
// ❌ WRONG: No debounce on search
|
|
313
|
-
function Search() {
|
|
314
|
-
const [query, setQuery] = useState('')
|
|
315
|
-
const { data } = useQuery({
|
|
316
|
-
queryKey: ['search', query], // Fires on every keystroke!
|
|
317
|
-
queryFn: () => search(query),
|
|
318
|
-
})
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// ✅ CORRECT: Debounced search
|
|
322
|
-
function Search() {
|
|
323
|
-
const [query, setQuery] = useState('')
|
|
324
|
-
const debouncedQuery = useDebounce(query, 300)
|
|
325
|
-
const { data } = useQuery({
|
|
326
|
-
queryKey: ['search', debouncedQuery],
|
|
327
|
-
queryFn: () => search(debouncedQuery),
|
|
328
|
-
enabled: debouncedQuery.length > 0,
|
|
329
|
-
})
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// ❌ WRONG: Creating debounce inside render
|
|
333
|
-
function Component() {
|
|
334
|
-
const search = debounce(() => {}, 300) // New instance every render!
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// ✅ CORRECT: Use hook
|
|
338
|
-
function Component() {
|
|
339
|
-
const search = useDebouncedCallback(() => {}, 300)
|
|
340
|
-
}
|
|
341
|
-
```
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: TanStack Query Patterns
|
|
3
|
-
description: >-
|
|
4
|
-
Auto-enforce TanStack Query best practices with factory key pattern. Activates
|
|
5
|
-
when creating queries, mutations, managing server state, or implementing data
|
|
6
|
-
fetching in React applications.
|
|
7
|
-
version: 1.0.0
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# TanStack Query Patterns
|
|
11
|
-
|
|
12
|
-
This skill enforces TanStack Query best practices for server state management in React applications.
|
|
13
|
-
|
|
14
|
-
## Query Key Factory Pattern
|
|
15
|
-
|
|
16
|
-
The factory pattern provides type-safe, hierarchical query keys:
|
|
17
|
-
|
|
18
|
-
```typescript
|
|
19
|
-
// lib/query-keys.ts
|
|
20
|
-
export const queryKeys = {
|
|
21
|
-
posts: {
|
|
22
|
-
all: () => ['posts'] as const,
|
|
23
|
-
lists: () => [...queryKeys.posts.all(), 'list'] as const,
|
|
24
|
-
list: (filters: PostFilters) => [...queryKeys.posts.lists(), filters] as const,
|
|
25
|
-
details: () => [...queryKeys.posts.all(), 'detail'] as const,
|
|
26
|
-
detail: (id: string) => [...queryKeys.posts.details(), id] as const,
|
|
27
|
-
comments: (id: string) => [...queryKeys.posts.detail(id), 'comments'] as const,
|
|
28
|
-
},
|
|
29
|
-
users: {
|
|
30
|
-
all: () => ['users'] as const,
|
|
31
|
-
detail: (id: string) => [...queryKeys.users.all(), id] as const,
|
|
32
|
-
profile: () => [...queryKeys.users.all(), 'profile'] as const,
|
|
33
|
-
},
|
|
34
|
-
auth: {
|
|
35
|
-
session: () => ['auth', 'session'] as const,
|
|
36
|
-
},
|
|
37
|
-
} as const
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Query Options Factory
|
|
41
|
-
|
|
42
|
-
Define reusable query options for consistency:
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
// features/posts/queries/postQueries.ts
|
|
46
|
-
import { queryOptions } from '@tanstack/react-query'
|
|
47
|
-
import { queryKeys } from '@/lib/query-keys'
|
|
48
|
-
import { postApi } from '@/features/posts/api'
|
|
49
|
-
|
|
50
|
-
export const postQueryOptions = (postId: string) =>
|
|
51
|
-
queryOptions({
|
|
52
|
-
queryKey: queryKeys.posts.detail(postId),
|
|
53
|
-
queryFn: () => postApi.getPost(postId),
|
|
54
|
-
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
export const postsQueryOptions = (filters: PostFilters = {}) =>
|
|
58
|
-
queryOptions({
|
|
59
|
-
queryKey: queryKeys.posts.list(filters),
|
|
60
|
-
queryFn: () => postApi.getPosts(filters),
|
|
61
|
-
staleTime: 1 * 60 * 1000, // 1 minute
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
export const postCommentsQueryOptions = (postId: string) =>
|
|
65
|
-
queryOptions({
|
|
66
|
-
queryKey: queryKeys.posts.comments(postId),
|
|
67
|
-
queryFn: () => postApi.getPostComments(postId),
|
|
68
|
-
enabled: Boolean(postId),
|
|
69
|
-
})
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
## Query Client Setup
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
75
|
-
// lib/query-client.ts
|
|
76
|
-
import { QueryClient } from '@tanstack/react-query'
|
|
77
|
-
|
|
78
|
-
export function createQueryClient() {
|
|
79
|
-
return new QueryClient({
|
|
80
|
-
defaultOptions: {
|
|
81
|
-
queries: {
|
|
82
|
-
staleTime: 60 * 1000, // 1 minute
|
|
83
|
-
gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime)
|
|
84
|
-
retry: 1,
|
|
85
|
-
refetchOnWindowFocus: false,
|
|
86
|
-
},
|
|
87
|
-
mutations: {
|
|
88
|
-
retry: 0,
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
})
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Using Queries in Components
|
|
96
|
-
|
|
97
|
-
### With useSuspenseQuery (Recommended with Router)
|
|
98
|
-
```typescript
|
|
99
|
-
import { useSuspenseQuery } from '@tanstack/react-query'
|
|
100
|
-
import { postQueryOptions } from '@/features/posts/queries'
|
|
101
|
-
|
|
102
|
-
function PostDetail({ postId }: { postId: string }) {
|
|
103
|
-
// Data guaranteed by route loader, suspense handles loading
|
|
104
|
-
const { data: post } = useSuspenseQuery(postQueryOptions(postId))
|
|
105
|
-
|
|
106
|
-
return <article>{post.title}</article>
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### With useQuery (Manual Loading States)
|
|
111
|
-
```typescript
|
|
112
|
-
import { useQuery } from '@tanstack/react-query'
|
|
113
|
-
import { postsQueryOptions } from '@/features/posts/queries'
|
|
114
|
-
|
|
115
|
-
function PostList({ filters }: { filters: PostFilters }) {
|
|
116
|
-
const { data, isLoading, error } = useQuery(postsQueryOptions(filters))
|
|
117
|
-
|
|
118
|
-
if (isLoading) return <Skeleton />
|
|
119
|
-
if (error) return <Error error={error} />
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<ul>
|
|
123
|
-
{data.map(post => (
|
|
124
|
-
<PostCard key={post.id} post={post} />
|
|
125
|
-
))}
|
|
126
|
-
</ul>
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Mutations
|
|
132
|
-
|
|
133
|
-
### Basic Mutation Hook
|
|
134
|
-
```typescript
|
|
135
|
-
// features/posts/hooks/useCreatePost.ts
|
|
136
|
-
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
137
|
-
import { queryKeys } from '@/lib/query-keys'
|
|
138
|
-
import { postApi } from '@/features/posts/api'
|
|
139
|
-
|
|
140
|
-
export function useCreatePost() {
|
|
141
|
-
const queryClient = useQueryClient()
|
|
142
|
-
|
|
143
|
-
return useMutation({
|
|
144
|
-
mutationFn: postApi.createPost,
|
|
145
|
-
onSuccess: () => {
|
|
146
|
-
// Invalidate all post lists
|
|
147
|
-
queryClient.invalidateQueries({ queryKey: queryKeys.posts.lists() })
|
|
148
|
-
},
|
|
149
|
-
})
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### Optimistic Updates
|
|
154
|
-
```typescript
|
|
155
|
-
// features/posts/hooks/useUpdatePost.ts
|
|
156
|
-
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
157
|
-
import { queryKeys } from '@/lib/query-keys'
|
|
158
|
-
import type { Post, UpdatePostInput } from '@/features/posts/types'
|
|
159
|
-
|
|
160
|
-
export function useUpdatePost() {
|
|
161
|
-
const queryClient = useQueryClient()
|
|
162
|
-
|
|
163
|
-
return useMutation({
|
|
164
|
-
mutationFn: postApi.updatePost,
|
|
165
|
-
onMutate: async (newPost: UpdatePostInput) => {
|
|
166
|
-
// Cancel outgoing refetches
|
|
167
|
-
await queryClient.cancelQueries({
|
|
168
|
-
queryKey: queryKeys.posts.detail(newPost.id)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
// Snapshot previous value
|
|
172
|
-
const previous = queryClient.getQueryData<Post>(
|
|
173
|
-
queryKeys.posts.detail(newPost.id)
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
// Optimistically update
|
|
177
|
-
queryClient.setQueryData(
|
|
178
|
-
queryKeys.posts.detail(newPost.id),
|
|
179
|
-
(old: Post | undefined) => old ? { ...old, ...newPost } : undefined
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
return { previous }
|
|
183
|
-
},
|
|
184
|
-
onError: (err, newPost, context) => {
|
|
185
|
-
// Rollback on error
|
|
186
|
-
if (context?.previous) {
|
|
187
|
-
queryClient.setQueryData(
|
|
188
|
-
queryKeys.posts.detail(newPost.id),
|
|
189
|
-
context.previous
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
},
|
|
193
|
-
onSettled: (data, error, variables) => {
|
|
194
|
-
// Always refetch after error or success
|
|
195
|
-
queryClient.invalidateQueries({
|
|
196
|
-
queryKey: queryKeys.posts.detail(variables.id)
|
|
197
|
-
})
|
|
198
|
-
},
|
|
199
|
-
})
|
|
200
|
-
}
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### Delete with Optimistic Update
|
|
204
|
-
```typescript
|
|
205
|
-
export function useDeletePost() {
|
|
206
|
-
const queryClient = useQueryClient()
|
|
207
|
-
|
|
208
|
-
return useMutation({
|
|
209
|
-
mutationFn: postApi.deletePost,
|
|
210
|
-
onMutate: async (postId: string) => {
|
|
211
|
-
await queryClient.cancelQueries({ queryKey: queryKeys.posts.lists() })
|
|
212
|
-
|
|
213
|
-
const previousLists = queryClient.getQueriesData<Post[]>({
|
|
214
|
-
queryKey: queryKeys.posts.lists()
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
// Remove from all lists optimistically
|
|
218
|
-
queryClient.setQueriesData<Post[]>(
|
|
219
|
-
{ queryKey: queryKeys.posts.lists() },
|
|
220
|
-
(old) => old?.filter(post => post.id !== postId)
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
return { previousLists }
|
|
224
|
-
},
|
|
225
|
-
onError: (err, postId, context) => {
|
|
226
|
-
context?.previousLists.forEach(([queryKey, data]) => {
|
|
227
|
-
queryClient.setQueryData(queryKey, data)
|
|
228
|
-
})
|
|
229
|
-
},
|
|
230
|
-
onSettled: () => {
|
|
231
|
-
queryClient.invalidateQueries({ queryKey: queryKeys.posts.all() })
|
|
232
|
-
},
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
## Prefetching
|
|
238
|
-
|
|
239
|
-
### In Route Loaders
|
|
240
|
-
```typescript
|
|
241
|
-
// routes/posts.tsx
|
|
242
|
-
export const Route = createFileRoute('/posts')({
|
|
243
|
-
loader: ({ context: { queryClient } }) =>
|
|
244
|
-
queryClient.ensureQueryData(postsQueryOptions()),
|
|
245
|
-
})
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
### On Hover/Focus
|
|
249
|
-
```typescript
|
|
250
|
-
import { useQueryClient } from '@tanstack/react-query'
|
|
251
|
-
import { Link } from '@tanstack/react-router'
|
|
252
|
-
import { postQueryOptions } from '@/features/posts/queries'
|
|
253
|
-
|
|
254
|
-
function PostLink({ postId, title }: { postId: string; title: string }) {
|
|
255
|
-
const queryClient = useQueryClient()
|
|
256
|
-
|
|
257
|
-
const prefetch = () => {
|
|
258
|
-
queryClient.prefetchQuery(postQueryOptions(postId))
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return (
|
|
262
|
-
<Link
|
|
263
|
-
to="/posts/$postId"
|
|
264
|
-
params={{ postId }}
|
|
265
|
-
onMouseEnter={prefetch}
|
|
266
|
-
onFocus={prefetch}
|
|
267
|
-
>
|
|
268
|
-
{title}
|
|
269
|
-
</Link>
|
|
270
|
-
)
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
## Infinite Queries
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
import { useInfiniteQuery } from '@tanstack/react-query'
|
|
278
|
-
|
|
279
|
-
export function useInfinitePosts(filters: PostFilters) {
|
|
280
|
-
return useInfiniteQuery({
|
|
281
|
-
queryKey: queryKeys.posts.list({ ...filters, infinite: true }),
|
|
282
|
-
queryFn: ({ pageParam = 1 }) =>
|
|
283
|
-
postApi.getPosts({ ...filters, page: pageParam }),
|
|
284
|
-
getNextPageParam: (lastPage, pages) =>
|
|
285
|
-
lastPage.hasMore ? pages.length + 1 : undefined,
|
|
286
|
-
initialPageParam: 1,
|
|
287
|
-
})
|
|
288
|
-
}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
## Dependent Queries
|
|
292
|
-
|
|
293
|
-
```typescript
|
|
294
|
-
function PostWithAuthor({ postId }: { postId: string }) {
|
|
295
|
-
const { data: post } = useQuery(postQueryOptions(postId))
|
|
296
|
-
|
|
297
|
-
const { data: author } = useQuery({
|
|
298
|
-
...userQueryOptions(post?.authorId ?? ''),
|
|
299
|
-
enabled: Boolean(post?.authorId),
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
if (!post) return <Skeleton />
|
|
303
|
-
|
|
304
|
-
return (
|
|
305
|
-
<article>
|
|
306
|
-
<h1>{post.title}</h1>
|
|
307
|
-
{author && <p>By {author.name}</p>}
|
|
308
|
-
</article>
|
|
309
|
-
)
|
|
310
|
-
}
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
## Conventions to Enforce
|
|
314
|
-
|
|
315
|
-
1. **Factory pattern for keys** - All query keys through `queryKeys` factory
|
|
316
|
-
2. **Query options factories** - Define in `features/*/queries/` directories
|
|
317
|
-
3. **Invalidate by hierarchy** - Use `queryKeys.posts.lists()` to invalidate all lists
|
|
318
|
-
4. **Optimistic updates** - Always include rollback logic
|
|
319
|
-
5. **Enable conditional queries** - Use `enabled` option for dependent queries
|
|
320
|
-
6. **Proper staleTime** - Set based on data freshness requirements
|
|
321
|
-
7. **Use gcTime not cacheTime** - Renamed in v5
|
|
322
|
-
|
|
323
|
-
## Anti-Patterns to Block
|
|
324
|
-
|
|
325
|
-
```typescript
|
|
326
|
-
// ❌ WRONG: String query keys
|
|
327
|
-
useQuery({ queryKey: ['posts', postId] })
|
|
328
|
-
|
|
329
|
-
// ✅ CORRECT: Factory pattern
|
|
330
|
-
useQuery(postQueryOptions(postId))
|
|
331
|
-
|
|
332
|
-
// ❌ WRONG: Inline query function
|
|
333
|
-
useQuery({
|
|
334
|
-
queryKey: queryKeys.posts.detail(postId),
|
|
335
|
-
queryFn: async () => {
|
|
336
|
-
const res = await fetch(`/api/posts/${postId}`)
|
|
337
|
-
return res.json()
|
|
338
|
-
}
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
// ✅ CORRECT: Extracted to API layer
|
|
342
|
-
useQuery(postQueryOptions(postId))
|
|
343
|
-
|
|
344
|
-
// ❌ WRONG: Manual cache updates without invalidation
|
|
345
|
-
onSuccess: (newPost) => {
|
|
346
|
-
queryClient.setQueryData(['posts', newPost.id], newPost)
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// ✅ CORRECT: Invalidate to refetch
|
|
350
|
-
onSuccess: () => {
|
|
351
|
-
queryClient.invalidateQueries({ queryKey: queryKeys.posts.all() })
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ❌ WRONG: Using cacheTime (deprecated)
|
|
355
|
-
useQuery({ cacheTime: 5000 })
|
|
356
|
-
|
|
357
|
-
// ✅ CORRECT: Use gcTime
|
|
358
|
-
useQuery({ gcTime: 5000 })
|
|
359
|
-
```
|