@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.
Files changed (41) hide show
  1. package/dist/index.js +4 -2
  2. package/dist/template/.pi/AGENTS.md +1 -1
  3. package/dist/template/.pi/README.md +2 -3
  4. package/dist/template/.pi/VERSION +1 -1
  5. package/dist/template/.pi/agents/explore.md +1 -1
  6. package/dist/template/.pi/agents/scout.md +1 -1
  7. package/dist/template/.pi/extensions/templates-injector.ts +35 -7
  8. package/dist/template/.pi/prompts/INDEX.md +3 -9
  9. package/dist/template/.pi/prompts/gc.md +2 -1
  10. package/dist/template/.pi/prompts/verify.md +24 -0
  11. package/dist/template/.pi/skills/INDEX.md +40 -8
  12. package/dist/template/.pi/skills/dcp-hygiene/SKILL.md +1 -1
  13. package/dist/template/.pi/skills/frontend-design/SKILL.md +1 -1
  14. package/dist/template/.pi/skills/frontend-design/references/animation/motion-advanced.md +88 -15
  15. package/dist/template/.pi/skills/frontend-design/references/animation/motion-core.md +148 -13
  16. package/dist/template/.pi/skills/frontend-design/references/shadcn/setup.md +127 -20
  17. package/dist/template/.pi/skills/nextjs-app-router/SKILL.md +334 -0
  18. package/dist/template/.pi/skills/nextjs-cache/SKILL.md +262 -0
  19. package/dist/template/.pi/skills/react-best-practices/SKILL.md +79 -1
  20. package/dist/template/.pi/skills/react-compiler/SKILL.md +237 -0
  21. package/dist/template/.pi/skills/react-hook-form/SKILL.md +374 -0
  22. package/dist/template/.pi/skills/react-server-actions/SKILL.md +299 -0
  23. package/dist/template/.pi/skills/shadcn-ui/SKILL.md +404 -0
  24. package/dist/template/.pi/skills/tanstack-query/SKILL.md +330 -0
  25. package/dist/template/.pi/skills/v0/SKILL.md +264 -0
  26. package/dist/template/.pi/skills/zustand/SKILL.md +333 -0
  27. package/package.json +1 -1
  28. package/dist/template/.pi/context/fallow.md +0 -137
  29. package/dist/template/.pi/prompts/loop-check.md +0 -87
  30. package/dist/template/.pi/prompts/loop-init.md +0 -157
  31. package/dist/template/.pi/prompts/loop-review.md +0 -90
  32. package/dist/template/.pi/skills/loop-audit/SKILL.md +0 -141
  33. package/dist/template/.pi/skills/loop-cost/SKILL.md +0 -130
  34. package/dist/template/.pi/skills/loop-engineering/SKILL.md +0 -175
  35. package/dist/template/.pi/templates/loop-github-action.yml +0 -162
  36. package/dist/template/.pi/templates/loop-orchestrator.sh +0 -514
  37. package/dist/template/.pi/templates/loop-orchestrator.test.ts +0 -332
  38. package/dist/template/.pi/templates/loop-orchestrator.ts +0 -936
  39. package/dist/template/.pi/templates/loop-state.json +0 -24
  40. package/dist/template/.pi/templates/loop-state.md +0 -98
  41. 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: