@minhduydev/mdpi 0.4.0 → 0.5.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/dist/index.js +1 -1
- package/dist/template/.pi/VERSION +1 -1
- package/dist/template/.pi/extensions/templates-injector.ts +34 -6
- package/dist/template/.pi/prompts/INDEX.md +3 -9
- package/dist/template/.pi/skills/INDEX.md +81 -19
- package/dist/template/.pi/skills/accessibility-audit/SKILL.md +8 -2
- package/dist/template/.pi/skills/baseline-ui/SKILL.md +211 -0
- package/dist/template/.pi/skills/dcp-hygiene/SKILL.md +1 -1
- package/dist/template/.pi/skills/design-taste-frontend/SKILL.md +53 -42
- package/dist/template/.pi/skills/fixing-accessibility/SKILL.md +509 -0
- package/dist/template/.pi/skills/frontend-design/SKILL.md +60 -47
- package/dist/template/.pi/skills/frontend-design/references/animation/motion-advanced.md +88 -15
- package/dist/template/.pi/skills/frontend-design/references/animation/motion-core.md +148 -13
- package/dist/template/.pi/skills/frontend-design/references/shadcn/setup.md +127 -20
- package/dist/template/.pi/skills/frontend-ui-engineering/SKILL.md +21 -27
- package/dist/template/.pi/skills/nextjs-app-router/SKILL.md +334 -0
- package/dist/template/.pi/skills/nextjs-cache/SKILL.md +262 -0
- package/dist/template/.pi/skills/oklch-color-workflow/SKILL.md +426 -0
- package/dist/template/.pi/skills/production-hardening/SKILL.md +652 -0
- package/dist/template/.pi/skills/react-best-practices/SKILL.md +79 -1
- package/dist/template/.pi/skills/react-compiler/SKILL.md +237 -0
- package/dist/template/.pi/skills/react-hook-form/SKILL.md +374 -0
- package/dist/template/.pi/skills/react-server-actions/SKILL.md +299 -0
- package/dist/template/.pi/skills/shadcn-ui/SKILL.md +404 -0
- package/dist/template/.pi/skills/tanstack-query/SKILL.md +330 -0
- package/dist/template/.pi/skills/ui-craft-principles/SKILL.md +564 -0
- package/dist/template/.pi/skills/ui-quality-audit/SKILL.md +329 -0
- package/dist/template/.pi/skills/v0/SKILL.md +264 -0
- package/dist/template/.pi/skills/zustand/SKILL.md +333 -0
- package/dist/template/.pi/templates/DESIGN.md +76 -0
- package/dist/template/.pi/workflows/INDEX.md +2 -1
- package/dist/template/.pi/workflows/frontend-feature-workflow.md +343 -0
- package/dist/template/.pi/workflows/quality-loop.md +1 -1
- package/package.json +1 -1
- package/dist/template/.pi/prompts/loop-check.md +0 -87
- package/dist/template/.pi/prompts/loop-init.md +0 -157
- package/dist/template/.pi/prompts/loop-review.md +0 -90
- package/dist/template/.pi/skills/loop-audit/SKILL.md +0 -141
- package/dist/template/.pi/skills/loop-cost/SKILL.md +0 -130
- package/dist/template/.pi/skills/loop-engineering/SKILL.md +0 -175
- package/dist/template/.pi/templates/loop-github-action.yml +0 -162
- package/dist/template/.pi/templates/loop-orchestrator.sh +0 -514
- package/dist/template/.pi/templates/loop-orchestrator.test.ts +0 -332
- package/dist/template/.pi/templates/loop-orchestrator.ts +0 -936
- package/dist/template/.pi/templates/loop-state.json +0 -24
- package/dist/template/.pi/templates/loop-state.md +0 -98
- package/dist/template/.pi/templates/loop-vision.md +0 -110
- /package/dist/template/.pi/templates/{design.md → feature-design.md} +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-query
|
|
3
|
+
description: Use when implementing data fetching, caching, or mutations with TanStack Query v5. Covers useQuery, useMutation, optimistic updates, infinite queries, prefetching, SSR patterns, query keys. MUST load before any data fetching implementation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TanStack Query v5
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
- Fetching data from APIs in React applications
|
|
11
|
+
- Managing server state with automatic caching and background refetching
|
|
12
|
+
- Implementing optimistic updates for mutations
|
|
13
|
+
- Handling pagination and infinite scrolling
|
|
14
|
+
- Prefetching data for faster navigation
|
|
15
|
+
- Server-side rendering with client hydration
|
|
16
|
+
|
|
17
|
+
## When NOT to Use
|
|
18
|
+
|
|
19
|
+
- Server Components with direct DB access (use `use cache` instead)
|
|
20
|
+
- WebSocket-only real-time data (use SWR subscription or custom hook)
|
|
21
|
+
- Form state management (use React Hook Form)
|
|
22
|
+
- Global client-only state (use Zustand)
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
// app/providers.tsx
|
|
28
|
+
'use client'
|
|
29
|
+
|
|
30
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
31
|
+
import { useState } from 'react'
|
|
32
|
+
|
|
33
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
34
|
+
const [queryClient] = useState(
|
|
35
|
+
() => new QueryClient({
|
|
36
|
+
defaultOptions: {
|
|
37
|
+
queries: {
|
|
38
|
+
staleTime: 60 * 1000, // 1 min before considered stale
|
|
39
|
+
gcTime: 5 * 60 * 1000, // 5 min garbage collection
|
|
40
|
+
retry: 1, // One retry on failure
|
|
41
|
+
refetchOnWindowFocus: false,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<QueryClientProvider client={queryClient}>
|
|
49
|
+
{children}
|
|
50
|
+
</QueryClientProvider>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// app/layout.tsx
|
|
57
|
+
import { Providers } from './providers'
|
|
58
|
+
|
|
59
|
+
export default function RootLayout({ children }) {
|
|
60
|
+
return (
|
|
61
|
+
<html>
|
|
62
|
+
<body>
|
|
63
|
+
<Providers>{children}</Providers>
|
|
64
|
+
</body>
|
|
65
|
+
</html>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## useQuery — Basic Data Fetching
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
'use client'
|
|
74
|
+
|
|
75
|
+
import { useQuery } from '@tanstack/react-query'
|
|
76
|
+
|
|
77
|
+
function PostsList() {
|
|
78
|
+
const { data, isLoading, error } = useQuery({
|
|
79
|
+
queryKey: ['posts'],
|
|
80
|
+
queryFn: () => fetch('/api/posts').then(res => res.json()),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if (isLoading) return <PostsSkeleton />
|
|
84
|
+
if (error) return <ErrorDisplay error={error} />
|
|
85
|
+
|
|
86
|
+
return data.map(post => <PostCard key={post.id} post={post} />)
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Query Key Design
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// Flat keys — simple
|
|
94
|
+
useQuery({ queryKey: ['posts'], queryFn: fetchPosts })
|
|
95
|
+
|
|
96
|
+
// Hierarchical keys — filterable
|
|
97
|
+
useQuery({ queryKey: ['posts', { status: 'published' }], queryFn: () => fetchPosts('published') })
|
|
98
|
+
|
|
99
|
+
// Detail queries — id-based
|
|
100
|
+
useQuery({ queryKey: ['posts', postId], queryFn: () => fetchPost(postId) })
|
|
101
|
+
|
|
102
|
+
// Factory pattern — recommended for large apps
|
|
103
|
+
const postKeys = {
|
|
104
|
+
all: ['posts'] as const,
|
|
105
|
+
lists: () => [...postKeys.all, 'list'] as const,
|
|
106
|
+
list: (filters: Filters) => [...postKeys.lists(), filters] as const,
|
|
107
|
+
details: () => [...postKeys.all, 'detail'] as const,
|
|
108
|
+
detail: (id: string) => [...postKeys.details(), id] as const,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Usage:
|
|
112
|
+
useQuery({ queryKey: postKeys.lists({ status: 'published' }), ... })
|
|
113
|
+
useQuery({ queryKey: postKeys.detail(id), ... })
|
|
114
|
+
|
|
115
|
+
// Invalidation
|
|
116
|
+
queryClient.invalidateQueries({ queryKey: postKeys.lists() }) // All lists
|
|
117
|
+
queryClient.invalidateQueries({ queryKey: postKeys.detail(id) }) // Specific post
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## useMutation
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
124
|
+
|
|
125
|
+
function CreatePost() {
|
|
126
|
+
const queryClient = useQueryClient()
|
|
127
|
+
|
|
128
|
+
const mutation = useMutation({
|
|
129
|
+
mutationFn: (newPost: PostInput) =>
|
|
130
|
+
fetch('/api/posts', {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
body: JSON.stringify(newPost),
|
|
133
|
+
}),
|
|
134
|
+
|
|
135
|
+
// Invalidate and refetch after success
|
|
136
|
+
onSuccess: () => {
|
|
137
|
+
queryClient.invalidateQueries({ queryKey: ['posts'] })
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<button
|
|
143
|
+
disabled={mutation.isPending}
|
|
144
|
+
onClick={() => mutation.mutate({ title: 'New Post' })}
|
|
145
|
+
>
|
|
146
|
+
{mutation.isPending ? 'Creating...' : 'Create Post'}
|
|
147
|
+
</button>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Optimistic Updates
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
const mutation = useMutation({
|
|
156
|
+
mutationFn: toggleTodoStatus,
|
|
157
|
+
|
|
158
|
+
onMutate: async (todoId) => {
|
|
159
|
+
// Cancel outgoing refetches
|
|
160
|
+
await queryClient.cancelQueries({ queryKey: ['todos'] })
|
|
161
|
+
|
|
162
|
+
// Snapshot previous value
|
|
163
|
+
const previousTodos = queryClient.getQueryData(['todos'])
|
|
164
|
+
|
|
165
|
+
// Optimistically update
|
|
166
|
+
queryClient.setQueryData(['todos'], (old: Todo[]) =>
|
|
167
|
+
old.map(t => t.id === todoId ? { ...t, done: !t.done } : t)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
// Return context for rollback
|
|
171
|
+
return { previousTodos }
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
onError: (err, todoId, context) => {
|
|
175
|
+
// Rollback on failure
|
|
176
|
+
queryClient.setQueryData(['todos'], context?.previousTodos)
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
onSettled: () => {
|
|
180
|
+
// Refetch to sync with server
|
|
181
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Infinite Queries
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
import { useInfiniteQuery } from '@tanstack/react-query'
|
|
190
|
+
|
|
191
|
+
function PostFeed() {
|
|
192
|
+
const {
|
|
193
|
+
data,
|
|
194
|
+
fetchNextPage,
|
|
195
|
+
hasNextPage,
|
|
196
|
+
isFetchingNextPage,
|
|
197
|
+
} = useInfiniteQuery({
|
|
198
|
+
queryKey: ['posts', 'infinite'],
|
|
199
|
+
queryFn: ({ pageParam }) =>
|
|
200
|
+
fetch(`/api/posts?cursor=${pageParam}`).then(r => r.json()),
|
|
201
|
+
initialPageParam: 0,
|
|
202
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div>
|
|
207
|
+
{data.pages.map(page =>
|
|
208
|
+
page.posts.map(post => <PostCard key={post.id} post={post} />)
|
|
209
|
+
)}
|
|
210
|
+
{hasNextPage && (
|
|
211
|
+
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
|
|
212
|
+
Load more
|
|
213
|
+
</button>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Prefetching (Next.js)
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
// app/posts/page.tsx — Server Component
|
|
224
|
+
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
|
|
225
|
+
import { PostsList } from './PostsList'
|
|
226
|
+
|
|
227
|
+
export default async function PostsPage() {
|
|
228
|
+
const queryClient = new QueryClient()
|
|
229
|
+
|
|
230
|
+
// Prefetch on server
|
|
231
|
+
await queryClient.prefetchQuery({
|
|
232
|
+
queryKey: ['posts'],
|
|
233
|
+
queryFn: fetchPosts,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<HydrationBoundary state={dehydrate(queryClient)}>
|
|
238
|
+
<PostsList />
|
|
239
|
+
</HydrationBoundary>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
// app/posts/PostsList.tsx — Client Component
|
|
246
|
+
'use client'
|
|
247
|
+
|
|
248
|
+
import { useQuery } from '@tanstack/react-query'
|
|
249
|
+
|
|
250
|
+
export function PostsList() {
|
|
251
|
+
// Uses prefetched data — no loading state on first render
|
|
252
|
+
const { data } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts })
|
|
253
|
+
return data.map(post => <PostCard key={post.id} post={post} />)
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Combining with Server Actions
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
// Use Server Actions for mutations, TanStack Query for reads:
|
|
261
|
+
|
|
262
|
+
'use client'
|
|
263
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
264
|
+
import { createPost } from './actions' // Server Action
|
|
265
|
+
|
|
266
|
+
function PostsPage() {
|
|
267
|
+
const queryClient = useQueryClient()
|
|
268
|
+
|
|
269
|
+
// Read — TanStack Query
|
|
270
|
+
const posts = useQuery({
|
|
271
|
+
queryKey: ['posts'],
|
|
272
|
+
queryFn: () => fetch('/api/posts').then(r => r.json()),
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
// Write — Server Action + cache revalidation
|
|
276
|
+
const mutation = useMutation({
|
|
277
|
+
mutationFn: (data: FormData) => createPost(null, data),
|
|
278
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<form action={mutation.mutate}>
|
|
283
|
+
<input name="title" />
|
|
284
|
+
<button>Create</button>
|
|
285
|
+
</form>
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## staleTime vs gcTime
|
|
291
|
+
|
|
292
|
+
| Setting | What it controls | Recommended |
|
|
293
|
+
|---------|-----------------|-------------|
|
|
294
|
+
| `staleTime` | How long before data is considered stale and refetched | 30s–5min depending on update frequency |
|
|
295
|
+
| `gcTime` | How long inactive data stays in cache before garbage collection | 5–30min (longer than staleTime) |
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
// Static data — rarely changes
|
|
299
|
+
staleTime: Infinity, gcTime: 30 * 60 * 1000,
|
|
300
|
+
|
|
301
|
+
// Dashboard — updates every few minutes
|
|
302
|
+
staleTime: 5 * 60 * 1000, gcTime: 30 * 60 * 1000,
|
|
303
|
+
|
|
304
|
+
// Real-time feed — updates frequently
|
|
305
|
+
staleTime: 30 * 1000, gcTime: 5 * 60 * 1000,
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Common Pitfalls
|
|
309
|
+
|
|
310
|
+
| Pitfall | Fix |
|
|
311
|
+
|---------|-----|
|
|
312
|
+
| Using TanStack Query in Server Components | Move to `'use client'` component |
|
|
313
|
+
| `queryKey` as `['posts']` everywhere — no granularity | Use structured keys: `['posts', 'list', filters]` |
|
|
314
|
+
| Not setting `staleTime` — defaults to 0 | Set sensible `staleTime` — default 0 refetches too often |
|
|
315
|
+
| Mixing `isLoading` and `isFetching` | `isLoading` = first load; `isFetching` = any fetch including background |
|
|
316
|
+
| `useQuery` for mutations | `useQuery` is for reads; use `useMutation` for writes |
|
|
317
|
+
| Mutating cache directly without rollback | Always implement `onMutate` snapshot + `onError` rollback |
|
|
318
|
+
| Missing `HydrationBoundary` for SSR | Server-prefetched data won't hydrate without it |
|
|
319
|
+
| `queryClient` recreated every render | Wrap in `useState(() => new QueryClient(...))` |
|
|
320
|
+
|
|
321
|
+
## Verification
|
|
322
|
+
|
|
323
|
+
- [ ] `QueryClientProvider` wraps the app in root layout
|
|
324
|
+
- [ ] `staleTime` and `gcTime` configured globally (not per-query unless needed)
|
|
325
|
+
- [ ] Query keys follow a structured pattern (all → lists → detail)
|
|
326
|
+
- [ ] Mutations call `invalidateQueries` or use optimistic updates
|
|
327
|
+
- [ ] Optimistic updates have rollback via `onError` + `onMutate` snapshot
|
|
328
|
+
- [ ] SSR pages use `HydrationBoundary` with `dehydrate`
|
|
329
|
+
- [ ] No `useQuery` in Server Components
|
|
330
|
+
- [ ] `isLoading` used for first-load skeleton; `isFetching` for background updates
|