@minhduydev/mdpi 0.4.1 → 0.6.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 +4 -2
- package/dist/template/.pi/AGENTS.md +1 -1
- package/dist/template/.pi/README.md +2 -3
- package/dist/template/.pi/VERSION +1 -1
- package/dist/template/.pi/agents/explore.md +1 -1
- package/dist/template/.pi/agents/scout.md +1 -1
- package/dist/template/.pi/extensions/templates-injector.ts +35 -7
- package/dist/template/.pi/prompts/INDEX.md +3 -9
- package/dist/template/.pi/prompts/gc.md +2 -1
- package/dist/template/.pi/prompts/verify.md +24 -0
- package/dist/template/.pi/skills/INDEX.md +40 -8
- package/dist/template/.pi/skills/dcp-hygiene/SKILL.md +1 -1
- package/dist/template/.pi/skills/frontend-design/SKILL.md +1 -1
- 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/nextjs-app-router/SKILL.md +334 -0
- package/dist/template/.pi/skills/nextjs-cache/SKILL.md +262 -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/v0/SKILL.md +264 -0
- package/dist/template/.pi/skills/zustand/SKILL.md +333 -0
- package/package.json +1 -1
- package/dist/template/.pi/context/fallow.md +0 -137
- 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
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nextjs-app-router
|
|
3
|
+
description: Use when building or refactoring Next.js App Router pages. Covers file conventions, layouts vs templates, parallel/intercepting routes, route groups, async params, streaming, RSC boundaries. MUST load before any App Router architecture work.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Next.js App Router Patterns (Next.js 15+)
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
- Building new pages in Next.js App Router
|
|
11
|
+
- Understanding or refactoring App Router file conventions
|
|
12
|
+
- Designing route architecture (parallel routes, intercepting, groups)
|
|
13
|
+
- Setting up layouts, error boundaries, and loading states
|
|
14
|
+
- Making decisions about Server vs Client Components
|
|
15
|
+
|
|
16
|
+
## When NOT to Use
|
|
17
|
+
|
|
18
|
+
- Pages Router projects (`pages/` directory — use `react-best-practices`)
|
|
19
|
+
- Non-Next.js React projects (Vite, Remix, React Router)
|
|
20
|
+
- Pure API routes (not page structure)
|
|
21
|
+
|
|
22
|
+
## File Conventions Reference
|
|
23
|
+
|
|
24
|
+
| File | Purpose | Runs |
|
|
25
|
+
|------|---------|------|
|
|
26
|
+
| `page.tsx` | Route's unique UI | Server (default) |
|
|
27
|
+
| `layout.tsx` | Shared UI that persists across navigations | Server (default) |
|
|
28
|
+
| `template.tsx` | Shared UI that remounts on navigation | Server (default) |
|
|
29
|
+
| `loading.tsx` | Suspense fallback while page loads | Server (default) |
|
|
30
|
+
| `error.tsx` | Error boundary for the segment | Client |
|
|
31
|
+
| `not-found.tsx` | 404 UI for the segment | Server (default) |
|
|
32
|
+
| `default.tsx` | Fallback for parallel routes | Server (default) |
|
|
33
|
+
| `route.tsx` | API endpoint for the segment | Server |
|
|
34
|
+
|
|
35
|
+
## Layout vs Template
|
|
36
|
+
|
|
37
|
+
**Layout**: persists across navigations, state preserved.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// app/dashboard/layout.tsx
|
|
41
|
+
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
42
|
+
return (
|
|
43
|
+
<div>
|
|
44
|
+
<nav>Sidebar — stays mounted</nav>
|
|
45
|
+
<main>{children}</main>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Template**: remounts on every navigation. Use when you need:
|
|
52
|
+
- Page transitions (AnimatePresence needs remount)
|
|
53
|
+
- `useEffect` that must re-run on navigation
|
|
54
|
+
- Resetting client state between pages
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// app/dashboard/template.tsx
|
|
58
|
+
'use client'
|
|
59
|
+
|
|
60
|
+
import { AnimatePresence, motion } from 'motion/react'
|
|
61
|
+
|
|
62
|
+
export default function Template({ children }: { children: React.ReactNode }) {
|
|
63
|
+
return (
|
|
64
|
+
<AnimatePresence mode="wait">
|
|
65
|
+
<motion.div
|
|
66
|
+
key={usePathname()}
|
|
67
|
+
initial={{ opacity: 0 }}
|
|
68
|
+
animate={{ opacity: 1 }}
|
|
69
|
+
exit={{ opacity: 0 }}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</motion.div>
|
|
73
|
+
</AnimatePresence>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Rule**: Layout wraps Template wraps Page: `layout.tsx > template.tsx > page.tsx`
|
|
79
|
+
|
|
80
|
+
## Route Groups — `(group)/`
|
|
81
|
+
|
|
82
|
+
Use parentheses to group routes without affecting the URL:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
app/
|
|
86
|
+
├── (marketing)/
|
|
87
|
+
│ ├── page.tsx → /
|
|
88
|
+
│ ├── about/page.tsx → /about
|
|
89
|
+
│ └── layout.tsx # Marketing layout
|
|
90
|
+
├── (dashboard)/
|
|
91
|
+
│ ├── dashboard/page.tsx → /dashboard
|
|
92
|
+
│ └── layout.tsx # Dashboard layout (different from marketing)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Parallel Routes — `@folder/`
|
|
96
|
+
|
|
97
|
+
Render multiple pages in the same layout simultaneously:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
app/
|
|
101
|
+
├── dashboard/
|
|
102
|
+
│ ├── layout.tsx # Accepts both props:
|
|
103
|
+
│ ├── @analytics/ # children, analytics, team
|
|
104
|
+
│ │ └── page.tsx
|
|
105
|
+
│ ├── @team/
|
|
106
|
+
│ │ └── page.tsx
|
|
107
|
+
│ └── page.tsx # Default children
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// app/dashboard/layout.tsx
|
|
112
|
+
export default function DashboardLayout(props: {
|
|
113
|
+
children: React.ReactNode
|
|
114
|
+
analytics: React.ReactNode
|
|
115
|
+
team: React.ReactNode
|
|
116
|
+
}) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="grid grid-cols-2">
|
|
119
|
+
<div>{props.children}</div>
|
|
120
|
+
<div>{props.analytics}</div>
|
|
121
|
+
<div>{props.team}</div>
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Each slot needs a `default.tsx` for initial load and unmatched routes.
|
|
128
|
+
|
|
129
|
+
## Intercepting Routes — `(.)folder/`
|
|
130
|
+
|
|
131
|
+
Render a route in the context of another without full navigation:
|
|
132
|
+
|
|
133
|
+
| Convention | Meaning |
|
|
134
|
+
|-----------|---------|
|
|
135
|
+
| `(.)folder/` | Same level |
|
|
136
|
+
| `(..)folder/` | One level up |
|
|
137
|
+
| `(..)(..)folder/` | Two levels up |
|
|
138
|
+
| `(...)folder/` | From root |
|
|
139
|
+
|
|
140
|
+
Common pattern: Photo modal:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
app/
|
|
144
|
+
├── photos/
|
|
145
|
+
│ ├── page.tsx # Photos grid: /
|
|
146
|
+
│ └── [id]/
|
|
147
|
+
│ └── page.tsx # Photo detail: /photos/1
|
|
148
|
+
└── @modal/
|
|
149
|
+
├── default.tsx # null return
|
|
150
|
+
└── (.)photos/
|
|
151
|
+
└── [id]/
|
|
152
|
+
└── page.tsx # Modal overlay when navigating from photos grid
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
// app/layout.tsx
|
|
157
|
+
export default function RootLayout({ children, modal }) {
|
|
158
|
+
return (
|
|
159
|
+
<>
|
|
160
|
+
{children}
|
|
161
|
+
{modal}
|
|
162
|
+
</>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Async Server Components
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
// app/posts/[id]/page.tsx — no 'use client'
|
|
171
|
+
export default async function PostPage({
|
|
172
|
+
params,
|
|
173
|
+
}: {
|
|
174
|
+
params: Promise<{ id: string }> // params is a Promise in Next.js 15+
|
|
175
|
+
}) {
|
|
176
|
+
const { id } = await params
|
|
177
|
+
const post = await db.post.findUnique({ where: { id } })
|
|
178
|
+
|
|
179
|
+
return <article>{post.content}</article>
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Next.js 15+**: `params`, `searchParams` are Promises — must `await` them.
|
|
184
|
+
|
|
185
|
+
## Streaming with Suspense + loading.tsx
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
// app/posts/page.tsx
|
|
189
|
+
import { Suspense } from 'react'
|
|
190
|
+
|
|
191
|
+
export default function PostsPage() {
|
|
192
|
+
return (
|
|
193
|
+
<div>
|
|
194
|
+
<h1>Posts</h1>
|
|
195
|
+
<Suspense fallback={<PostsSkeleton />}>
|
|
196
|
+
<PostsList /> {/* Streams in when ready */}
|
|
197
|
+
</Suspense>
|
|
198
|
+
<Suspense fallback={<StatsSkeleton />}>
|
|
199
|
+
<PostStats /> {/* Streams independently */}
|
|
200
|
+
</Suspense>
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- `loading.tsx` wraps the entire page in Suspense automatically
|
|
207
|
+
- Manual `<Suspense>` gives finer control — stream sections independently
|
|
208
|
+
- Wrap data-fetching Server Components in Suspense
|
|
209
|
+
|
|
210
|
+
## RSC Boundary Rules
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
Server Component (default)
|
|
214
|
+
└─ can render → Client Components
|
|
215
|
+
└─ can pass → serializable props only (no functions, no JSX as props)
|
|
216
|
+
└─ can use → async/await, direct DB access, filesystem, secrets
|
|
217
|
+
|
|
218
|
+
Client Component ('use client')
|
|
219
|
+
└─ can render → Server Components (passed as children)
|
|
220
|
+
└─ can use → hooks, event handlers, browser APIs, state
|
|
221
|
+
└─ cannot → async/await directly, access DB
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Boundary pattern** — push 'use client' as deep as possible:
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
// ✅ Server Component (default) — data fetching here
|
|
228
|
+
export default async function UserProfile({ userId }) {
|
|
229
|
+
const user = await db.user.findUnique({ where: { id: userId } })
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div>
|
|
233
|
+
<h1>{user.name}</h1>
|
|
234
|
+
<EditButton /> {/* Only the interactive leaf is client */}
|
|
235
|
+
</div>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ✅ Client leaf — only what needs interactivity
|
|
240
|
+
'use client'
|
|
241
|
+
function EditButton() {
|
|
242
|
+
const [open, setOpen] = useState(false)
|
|
243
|
+
return <button onClick={() => setOpen(true)}>Edit</button>
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Error Handling
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
// app/dashboard/error.tsx
|
|
251
|
+
'use client'
|
|
252
|
+
|
|
253
|
+
export default function DashboardError({
|
|
254
|
+
error,
|
|
255
|
+
reset,
|
|
256
|
+
}: {
|
|
257
|
+
error: Error & { digest?: string }
|
|
258
|
+
reset: () => void
|
|
259
|
+
}) {
|
|
260
|
+
return (
|
|
261
|
+
<div>
|
|
262
|
+
<h2>Something went wrong!</h2>
|
|
263
|
+
<button onClick={() => reset()}>Try again</button>
|
|
264
|
+
</div>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
- `error.tsx` must be a Client Component
|
|
270
|
+
- Errors bubble up to the nearest `error.tsx`
|
|
271
|
+
- `reset()` re-renders the error boundary's children
|
|
272
|
+
- `error.tsx` in a nested segment only catches errors in that segment and below
|
|
273
|
+
|
|
274
|
+
## Middleware
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
// middleware.ts (root level)
|
|
278
|
+
import { NextResponse } from 'next/server'
|
|
279
|
+
import type { NextRequest } from 'next/server'
|
|
280
|
+
|
|
281
|
+
export function middleware(request: NextRequest) {
|
|
282
|
+
const token = request.cookies.get('token')
|
|
283
|
+
|
|
284
|
+
// Protect routes
|
|
285
|
+
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
286
|
+
return NextResponse.redirect(new URL('/login', request.url))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return NextResponse.next()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export const config = {
|
|
293
|
+
matcher: ['/dashboard/:path*']
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Metadata API
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
// Static metadata
|
|
301
|
+
export const metadata: Metadata = {
|
|
302
|
+
title: 'Dashboard',
|
|
303
|
+
description: 'Manage your account',
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Dynamic metadata (Server Components)
|
|
307
|
+
export async function generateMetadata({ params }): Promise<Metadata> {
|
|
308
|
+
const post = await getPost(params.id)
|
|
309
|
+
return { title: post.title, description: post.excerpt }
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Common Pitfalls
|
|
314
|
+
|
|
315
|
+
| Pitfall | Fix |
|
|
316
|
+
|---------|-----|
|
|
317
|
+
| Adding `'use client'` to a layout | Layouts are Server Components by default; only add `'use client'` when you need hooks |
|
|
318
|
+
| Forgetting `default.tsx` for parallel routes | Every slot needs a `default.tsx` for initial load |
|
|
319
|
+
| Passing functions as props from Server to Client | Functions are not serializable — define them in the client |
|
|
320
|
+
| Using `useSearchParams()` in Server Component | Use `searchParams` prop (Promise in v15+) |
|
|
321
|
+
| `params` treated as plain object (v15+) | `params` is now a Promise — must `await params` |
|
|
322
|
+
| No Suspense boundary for async component | Wrap in `<Suspense>` or ensure `loading.tsx` exists |
|
|
323
|
+
| `layout.tsx` doesn't receive `searchParams` | Only `page.tsx` gets `searchParams` |
|
|
324
|
+
|
|
325
|
+
## Verification
|
|
326
|
+
|
|
327
|
+
- [ ] Server Components are the default — `'use client'` only where interactive
|
|
328
|
+
- [ ] `params` and `searchParams` are awaited (Next.js 15+)
|
|
329
|
+
- [ ] Error boundaries (`error.tsx`) exist at appropriate levels
|
|
330
|
+
- [ ] Loading states (`loading.tsx` or `<Suspense>`) for async pages
|
|
331
|
+
- [ ] Parallel route slots each have `default.tsx`
|
|
332
|
+
- [ ] Layout and template used correctly (persist vs remount)
|
|
333
|
+
- [ ] Route groups used to share layouts without affecting URL
|
|
334
|
+
- [ ] Functions and non-serializable data stay in Client Components
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nextjs-cache
|
|
3
|
+
description: Use when working with Next.js 16 caching — `use cache` directive, cacheLife, cacheTag, revalidation, migration from v15. MUST load before implementing any caching strategy.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Next.js Cache System (Next.js 16)
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
- Adding `use cache` to functions or components
|
|
11
|
+
- Configuring cache lifetimes with `cacheLife()`
|
|
12
|
+
- Tagging cache entries for targeted revalidation
|
|
13
|
+
- Migrating from Next.js 15's implicit cache (force-dynamic, fetch cache)
|
|
14
|
+
- Calling `revalidateTag()` / `revalidatePath()` after mutations
|
|
15
|
+
- Using `connection()` for database-aware caching
|
|
16
|
+
|
|
17
|
+
## When NOT to Use
|
|
18
|
+
|
|
19
|
+
- Pages Router projects (no `use cache` support)
|
|
20
|
+
- Next.js 14 or earlier (different cache model)
|
|
21
|
+
- Purely client-side caching (use TanStack Query or SWR)
|
|
22
|
+
|
|
23
|
+
## Core Pattern: `use cache`
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
// app/lib/data.ts
|
|
27
|
+
import { cacheTag } from 'next/cache'
|
|
28
|
+
import { db } from '@/lib/db'
|
|
29
|
+
|
|
30
|
+
export async function getPosts() {
|
|
31
|
+
'use cache'
|
|
32
|
+
cacheTag('posts') // Tag for later revalidation
|
|
33
|
+
|
|
34
|
+
const posts = await db.post.findMany({
|
|
35
|
+
include: { author: true },
|
|
36
|
+
orderBy: { createdAt: 'desc' },
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return posts
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// app/posts/page.tsx
|
|
45
|
+
import { getPosts } from '@/lib/data'
|
|
46
|
+
|
|
47
|
+
export default async function PostsPage() {
|
|
48
|
+
const posts = await getPosts() // Cached until revalidated or expired
|
|
49
|
+
return <PostsList posts={posts} />
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## cacheLife — Set TTL
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { cacheLife } from 'next/cache'
|
|
57
|
+
|
|
58
|
+
export async function getPopularPosts() {
|
|
59
|
+
'use cache'
|
|
60
|
+
cacheLife('hours') // Revalidate every hour
|
|
61
|
+
cacheTag('popular-posts')
|
|
62
|
+
|
|
63
|
+
return db.post.findMany({ where: { views: { gte: 1000 } } })
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Available `cacheLife` values:
|
|
68
|
+
|
|
69
|
+
| Profile | Duration | Use Case |
|
|
70
|
+
|---------|----------|----------|
|
|
71
|
+
| `'seconds'` | ~1 second | Real-time but deduped |
|
|
72
|
+
| `'minutes'` | ~5 minutes | Frequently changing data |
|
|
73
|
+
| `'hours'` | ~1 hour | Dashboard stats, user profiles |
|
|
74
|
+
| `'days'` | ~1 day | Blog content, static pages |
|
|
75
|
+
| `'weeks'` | ~1 week | Changelogs, documentation |
|
|
76
|
+
| `'max'` | Unlimited | Immutable data (never revalidates automatically) |
|
|
77
|
+
|
|
78
|
+
Custom profiles via `next.config.ts`:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// next.config.ts
|
|
82
|
+
import type { NextConfig } from 'next'
|
|
83
|
+
|
|
84
|
+
const config: NextConfig = {
|
|
85
|
+
cacheLife: {
|
|
86
|
+
frequent: {
|
|
87
|
+
stale: 60, // seconds before background revalidate
|
|
88
|
+
revalidate: 300, // seconds before full re-fetch
|
|
89
|
+
expire: 3600, // seconds before purge
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## cacheTag — Target Revalidation
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
// Tag multiple related caches
|
|
99
|
+
export async function getPost(id: string) {
|
|
100
|
+
'use cache'
|
|
101
|
+
cacheTag(`post-${id}`, 'posts') // Individual + list tag
|
|
102
|
+
|
|
103
|
+
return db.post.findUnique({ where: { id } })
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
// app/actions.ts — revalidate after mutation
|
|
109
|
+
'use server'
|
|
110
|
+
|
|
111
|
+
import { revalidateTag, revalidatePath } from 'next/cache'
|
|
112
|
+
|
|
113
|
+
export async function deletePost(id: string) {
|
|
114
|
+
await db.post.delete({ where: { id } })
|
|
115
|
+
|
|
116
|
+
revalidateTag(`post-${id}`) // Revalidate specific post
|
|
117
|
+
revalidateTag('posts') // Revalidate all post lists
|
|
118
|
+
revalidatePath('/posts') // Also revalidate the URL path
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## connection() — Database-Driven Cache
|
|
123
|
+
|
|
124
|
+
`connection()` makes the cache aware of your database connection, speeding up purge:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { connection } from 'next/cache'
|
|
128
|
+
|
|
129
|
+
export async function getPost(id: string) {
|
|
130
|
+
'use cache'
|
|
131
|
+
cacheTag(`post-${id}`)
|
|
132
|
+
connection() // Invalidate when DB connection changes (e.g., deploy)
|
|
133
|
+
|
|
134
|
+
return db.post.findUnique({ where: { id } })
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Call `connection()` at any point in the cached function. Multiple calls deduplicate.
|
|
139
|
+
|
|
140
|
+
## Cacheable vs Non-Cacheable
|
|
141
|
+
|
|
142
|
+
**Can be cached**: Database queries, filesystem reads, fetch to stable APIs, computed values, Component output.
|
|
143
|
+
|
|
144
|
+
**Cannot be cached**: Request objects (`cookies()`, `headers()`), mutable state, random values, real-time data.
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
export async function getUserData() {
|
|
148
|
+
const session = await auth() // ❌ Uses cookies — cannot cache
|
|
149
|
+
|
|
150
|
+
const user = await db.user.findUnique({
|
|
151
|
+
where: { id: session.userId }
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return user
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Solution**: Split the function — cache only the data part:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
export async function getUserData() {
|
|
162
|
+
const session = await auth() // Not cached
|
|
163
|
+
|
|
164
|
+
return getUser(session.userId) // Cached
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function getUser(id: string) {
|
|
168
|
+
'use cache'
|
|
169
|
+
cacheTag(`user-${id}`)
|
|
170
|
+
return db.user.findUnique({ where: { id } })
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Migration from Next.js 15
|
|
175
|
+
|
|
176
|
+
### Before (v15 implicit caching):
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// Next.js 15
|
|
180
|
+
export const dynamic = 'force-dynamic' // Opt out of caching
|
|
181
|
+
export const revalidate = 3600 // ISR interval
|
|
182
|
+
|
|
183
|
+
// fetch caching
|
|
184
|
+
const data = await fetch(url, { cache: 'no-store' })
|
|
185
|
+
const data = await fetch(url, { next: { revalidate: 60 } })
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### After (v16 explicit caching):
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
// Next.js 16 — everything is dynamic by default
|
|
192
|
+
// To cache, use `use cache`:
|
|
193
|
+
export async function getPage() {
|
|
194
|
+
'use cache'
|
|
195
|
+
cacheLife('hours')
|
|
196
|
+
return db.page.findMany()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// No more fetch cache options — use cache instead:
|
|
200
|
+
const data = await fetch(url) // Always fresh, unless wrapped in use cache
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Quick Migration Table
|
|
204
|
+
|
|
205
|
+
| v15 Pattern | v16 Equivalent |
|
|
206
|
+
|------------|----------------|
|
|
207
|
+
| `fetch(url, { cache: 'no-store' })` | `fetch(url)` — no cache (default) |
|
|
208
|
+
| `fetch(url, { next: { revalidate: 60 } })` | Wrap in `use cache` + `cacheLife` |
|
|
209
|
+
| `export const dynamic = 'force-dynamic'` | Remove — dynamic is default |
|
|
210
|
+
| `export const revalidate = 3600` | `'use cache'` + `cacheLife('hours')` |
|
|
211
|
+
| `revalidatePath('/posts')` | Same API — still works |
|
|
212
|
+
| `revalidateTag('posts')` | Same API — still works with `cacheTag` |
|
|
213
|
+
| `unstable_cache(fn, ['key'])` | `'use cache'` + `cacheTag('key')` |
|
|
214
|
+
|
|
215
|
+
## Revalidation vs Expiration
|
|
216
|
+
|
|
217
|
+
- **Revalidate** (`revalidateTag`/`revalidatePath`): Immediate purge and re-fetch on next request
|
|
218
|
+
- **Expire** (`cacheLife` TTL): Background revalidate, stale data served until fresh data ready
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
// Mutation pattern — revalidate affected caches
|
|
222
|
+
export async function updatePost(id: string, data: PostInput) {
|
|
223
|
+
await db.post.update({ where: { id }, data })
|
|
224
|
+
|
|
225
|
+
revalidateTag(`post-${id}`) // Immediate purge
|
|
226
|
+
// cacheLife handles TTL-based refresh for other entries
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Concurrent Mutations Safety
|
|
231
|
+
|
|
232
|
+
`use cache` functions support deduplication — concurrent requests for the same data share one database call:
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
// Three components call getPosts() on same page
|
|
236
|
+
// → only ONE database query executes
|
|
237
|
+
<PostsList /> // calls getPosts()
|
|
238
|
+
<RecentPosts /> // calls getPosts() — deduped
|
|
239
|
+
<PopularPosts /> // calls getPosts() — deduped
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Common Pitfalls
|
|
243
|
+
|
|
244
|
+
| Pitfall | Fix |
|
|
245
|
+
|---------|-----|
|
|
246
|
+
| Using `cookies()` inside `'use cache'` | Split function: read cookies outside, pass data into cached function |
|
|
247
|
+
| Forgetting `cacheTag()` | Without tags, cache can only be purged by TTL or full flush |
|
|
248
|
+
| Using `revalidatePath` too broadly | Prefer `revalidateTag` — narrower scope, less CPU |
|
|
249
|
+
| Not invalidating after mutation | Every mutation must revalidate affected caches |
|
|
250
|
+
| `cacheLife` too short for write-heavy data | Use `cacheLife('seconds')` or skip caching for hot data |
|
|
251
|
+
| Caching user-specific data without user-scoping | Include `userId` in `cacheTag`: `cacheTag('user-${id}-posts')` |
|
|
252
|
+
| Assuming cache survives deployment | Add `connection()` to auto-invalidate on deploy |
|
|
253
|
+
|
|
254
|
+
## Verification
|
|
255
|
+
|
|
256
|
+
- [ ] `'use cache'` functions have `cacheTag()` for targeted revalidation
|
|
257
|
+
- [ ] Mutations call `revalidateTag()` or `revalidatePath()`
|
|
258
|
+
- [ ] No `cookies()` or `headers()` inside cached functions
|
|
259
|
+
- [ ] `cacheLife` appropriate for data freshness requirements
|
|
260
|
+
- [ ] `connection()` added for database queries that should invalidate on deploy
|
|
261
|
+
- [ ] v15 `fetch` cache options removed or migrated
|
|
262
|
+
- [ ] User-scoped data uses user-specific cache tags
|
|
@@ -7,11 +7,16 @@ description: MUST load when writing, reviewing, or refactoring React/Next.js cod
|
|
|
7
7
|
|
|
8
8
|
## When to Use
|
|
9
9
|
|
|
10
|
-
- Applying performance guidelines to React/Next.js components or pages.
|
|
10
|
+
- Applying performance guidelines to React 19 / Next.js 16 components or pages.
|
|
11
|
+
- Optimizing Server Components, data fetching, bundle size, and re-renders.
|
|
11
12
|
|
|
12
13
|
## When NOT to Use
|
|
13
14
|
|
|
14
15
|
- Non-React codebases or UI-free/backend-only changes.
|
|
16
|
+
- Server Actions and form patterns (use `react-server-actions` skill)
|
|
17
|
+
- App Router architecture (use `nextjs-app-router` skill)
|
|
18
|
+
- Next.js 16 caching (use `nextjs-cache` skill)
|
|
19
|
+
- State management (use `tanstack-query`, `zustand`, or `react-hook-form` skills)
|
|
15
20
|
|
|
16
21
|
|
|
17
22
|
## When to Apply
|
|
@@ -108,6 +113,79 @@ Reference these guidelines when:
|
|
|
108
113
|
- `advanced-event-handler-refs` - Store event handlers in refs
|
|
109
114
|
- `advanced-use-latest` - useLatest for stable callback refs
|
|
110
115
|
|
|
116
|
+
## React 19 Patterns
|
|
117
|
+
|
|
118
|
+
### Server Components: Default Data Flow
|
|
119
|
+
|
|
120
|
+
React 19 + Next.js App Router defaults to **Server Components**. Keep data fetching on the server:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// ✅ Server Component — fetch data where it lives
|
|
124
|
+
// app/posts/page.tsx
|
|
125
|
+
export default async function PostsPage() {
|
|
126
|
+
const posts = await db.post.findMany() // Direct DB access
|
|
127
|
+
const config = await fetchConfig() // Private API calls (no CORS)
|
|
128
|
+
|
|
129
|
+
return <PostsList posts={posts} config={config} />
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Only add `'use client'` at the interactive leaves — push it as deep as possible.
|
|
134
|
+
|
|
135
|
+
### New Hooks Performance Impact
|
|
136
|
+
|
|
137
|
+
| Hook | Use Case | Performance Note |
|
|
138
|
+
|------|----------|-----------------|
|
|
139
|
+
| `useOptimistic` | Instant UI feedback | Replaces manual `useState` + revert logic |
|
|
140
|
+
| `useActionState` | Form submissions | Replaces `useFormState` (deprecated) |
|
|
141
|
+
| `useFormStatus` | Pending states | Read from child, not form component |
|
|
142
|
+
| `use()` | Unwrap promises in render | Only in Client Components — Server Components use `await` |
|
|
143
|
+
|
|
144
|
+
### React Compiler Awareness
|
|
145
|
+
|
|
146
|
+
The React Compiler (stable, React 19+) auto-memoizes components and hooks. With compiler enabled:
|
|
147
|
+
|
|
148
|
+
- **Remove** manual `useMemo`, `useCallback`, `memo()` unless truly expensive
|
|
149
|
+
- **Keep** `useRef` (semantic, not memoization)
|
|
150
|
+
- **Keep** `useEffect` for synchronization (compiler doesn't touch effects)
|
|
151
|
+
- See `react-compiler` skill for full migration guide
|
|
152
|
+
|
|
153
|
+
### `use()` for Client Component Data
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
'use client'
|
|
157
|
+
|
|
158
|
+
import { use } from 'react'
|
|
159
|
+
|
|
160
|
+
function UserProfile({ userPromise }) {
|
|
161
|
+
const user = use(userPromise) // Unwrap promise in render
|
|
162
|
+
return <div>{user.name}</div>
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`use()` reads Promises and Context in render without a hook wrapper.
|
|
167
|
+
|
|
168
|
+
## Next.js 16 Caching
|
|
169
|
+
|
|
170
|
+
Next.js 16 reversed the v15 caching model — everything is dynamic by default. To cache:
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
// Cached — uses `use cache` directive
|
|
174
|
+
export async function getPosts() {
|
|
175
|
+
'use cache'
|
|
176
|
+
cacheLife('hours')
|
|
177
|
+
cacheTag('posts')
|
|
178
|
+
return db.post.findMany()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Dynamic — no directive (default)
|
|
182
|
+
export async function getUserSession() {
|
|
183
|
+
return auth() // Always fresh
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Migration**: Remove `export const dynamic = 'force-dynamic'` (now default). Replace `export const revalidate = 3600` with `'use cache'` + `cacheLife`. See `nextjs-cache` skill for detailed migration.
|
|
188
|
+
|
|
111
189
|
## How to Use
|
|
112
190
|
|
|
113
191
|
Read individual rule files for detailed explanations and code examples:
|