@devmunna/agent-skillkit 0.1.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 (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/bin/ai-skills.js +5 -0
  4. package/dist/cli/commands/add.d.ts +2 -0
  5. package/dist/cli/commands/add.d.ts.map +1 -0
  6. package/dist/cli/commands/add.js +66 -0
  7. package/dist/cli/commands/add.js.map +1 -0
  8. package/dist/cli/commands/doctor.d.ts +2 -0
  9. package/dist/cli/commands/doctor.d.ts.map +1 -0
  10. package/dist/cli/commands/doctor.js +33 -0
  11. package/dist/cli/commands/doctor.js.map +1 -0
  12. package/dist/cli/commands/init.d.ts +10 -0
  13. package/dist/cli/commands/init.d.ts.map +1 -0
  14. package/dist/cli/commands/init.js +145 -0
  15. package/dist/cli/commands/init.js.map +1 -0
  16. package/dist/cli/commands/list.d.ts +5 -0
  17. package/dist/cli/commands/list.d.ts.map +1 -0
  18. package/dist/cli/commands/list.js +55 -0
  19. package/dist/cli/commands/list.js.map +1 -0
  20. package/dist/cli/commands/update.d.ts +2 -0
  21. package/dist/cli/commands/update.d.ts.map +1 -0
  22. package/dist/cli/commands/update.js +49 -0
  23. package/dist/cli/commands/update.js.map +1 -0
  24. package/dist/cli/commands/validate.d.ts +2 -0
  25. package/dist/cli/commands/validate.d.ts.map +1 -0
  26. package/dist/cli/commands/validate.js +22 -0
  27. package/dist/cli/commands/validate.js.map +1 -0
  28. package/dist/cli/index.d.ts +2 -0
  29. package/dist/cli/index.d.ts.map +1 -0
  30. package/dist/cli/index.js +49 -0
  31. package/dist/cli/index.js.map +1 -0
  32. package/dist/cli/prompts/agent-selector.d.ts +3 -0
  33. package/dist/cli/prompts/agent-selector.d.ts.map +1 -0
  34. package/dist/cli/prompts/agent-selector.js +23 -0
  35. package/dist/cli/prompts/agent-selector.js.map +1 -0
  36. package/dist/cli/prompts/stack-selector.d.ts +3 -0
  37. package/dist/cli/prompts/stack-selector.d.ts.map +1 -0
  38. package/dist/cli/prompts/stack-selector.js +60 -0
  39. package/dist/cli/prompts/stack-selector.js.map +1 -0
  40. package/dist/core/config-manager.d.ts +20 -0
  41. package/dist/core/config-manager.d.ts.map +1 -0
  42. package/dist/core/config-manager.js +107 -0
  43. package/dist/core/config-manager.js.map +1 -0
  44. package/dist/core/detector.d.ts +3 -0
  45. package/dist/core/detector.d.ts.map +1 -0
  46. package/dist/core/detector.js +50 -0
  47. package/dist/core/detector.js.map +1 -0
  48. package/dist/core/doctor.d.ts +12 -0
  49. package/dist/core/doctor.d.ts.map +1 -0
  50. package/dist/core/doctor.js +102 -0
  51. package/dist/core/doctor.js.map +1 -0
  52. package/dist/core/skill-registry.d.ts +11 -0
  53. package/dist/core/skill-registry.d.ts.map +1 -0
  54. package/dist/core/skill-registry.js +174 -0
  55. package/dist/core/skill-registry.js.map +1 -0
  56. package/dist/core/skill-resolver.d.ts +3 -0
  57. package/dist/core/skill-resolver.d.ts.map +1 -0
  58. package/dist/core/skill-resolver.js +36 -0
  59. package/dist/core/skill-resolver.js.map +1 -0
  60. package/dist/core/validator.d.ts +13 -0
  61. package/dist/core/validator.d.ts.map +1 -0
  62. package/dist/core/validator.js +99 -0
  63. package/dist/core/validator.js.map +1 -0
  64. package/dist/generators/agent-installer.d.ts +5 -0
  65. package/dist/generators/agent-installer.d.ts.map +1 -0
  66. package/dist/generators/agent-installer.js +20 -0
  67. package/dist/generators/agent-installer.js.map +1 -0
  68. package/dist/generators/agents-md.d.ts +3 -0
  69. package/dist/generators/agents-md.d.ts.map +1 -0
  70. package/dist/generators/agents-md.js +70 -0
  71. package/dist/generators/agents-md.js.map +1 -0
  72. package/dist/generators/claude-md.d.ts +3 -0
  73. package/dist/generators/claude-md.d.ts.map +1 -0
  74. package/dist/generators/claude-md.js +47 -0
  75. package/dist/generators/claude-md.js.map +1 -0
  76. package/dist/generators/skill-generator.d.ts +5 -0
  77. package/dist/generators/skill-generator.d.ts.map +1 -0
  78. package/dist/generators/skill-generator.js +34 -0
  79. package/dist/generators/skill-generator.js.map +1 -0
  80. package/dist/generators/workflows.d.ts +3 -0
  81. package/dist/generators/workflows.d.ts.map +1 -0
  82. package/dist/generators/workflows.js +57 -0
  83. package/dist/generators/workflows.js.map +1 -0
  84. package/dist/index.d.ts +13 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +13 -0
  87. package/dist/index.js.map +1 -0
  88. package/dist/types/index.d.ts +55 -0
  89. package/dist/types/index.d.ts.map +1 -0
  90. package/dist/types/index.js +2 -0
  91. package/dist/types/index.js.map +1 -0
  92. package/dist/utils/file-utils.d.ts +12 -0
  93. package/dist/utils/file-utils.d.ts.map +1 -0
  94. package/dist/utils/file-utils.js +39 -0
  95. package/dist/utils/file-utils.js.map +1 -0
  96. package/dist/utils/logger.d.ts +10 -0
  97. package/dist/utils/logger.d.ts.map +1 -0
  98. package/dist/utils/logger.js +11 -0
  99. package/dist/utils/logger.js.map +1 -0
  100. package/package.json +73 -0
  101. package/skills/clean-architecture/SKILL.md +324 -0
  102. package/skills/express-mvc-prisma/SKILL.md +168 -0
  103. package/skills/express-mvc-prisma/references/auth.md +190 -0
  104. package/skills/express-mvc-prisma/references/boilerplate.md +196 -0
  105. package/skills/express-mvc-prisma/references/error-handling.md +121 -0
  106. package/skills/express-mvc-prisma/references/module-scaffold.md +253 -0
  107. package/skills/express-mvc-prisma/references/prisma-setup.md +97 -0
  108. package/skills/express-mvc-prisma/references/response-helpers.md +157 -0
  109. package/skills/express-mvc-prisma/references/zod-validation.md +157 -0
  110. package/skills/fastify-rest/SKILL.md +287 -0
  111. package/skills/mongoose-odm/SKILL.md +281 -0
  112. package/skills/nextjs-fullstack/SKILL.md +328 -0
  113. package/skills/nextjs-fullstack/references/auth.md +270 -0
  114. package/skills/nextjs-fullstack/references/caching.md +157 -0
  115. package/skills/nextjs-fullstack/references/route-handlers.md +194 -0
  116. package/skills/nextjs-fullstack/references/server-actions.md +214 -0
  117. package/skills/nextjs-fullstack/references/server-components.md +190 -0
  118. package/skills/node-base/SKILL.md +139 -0
  119. package/skills/prisma-orm/SKILL.md +334 -0
  120. package/skills/react-feature-arch/SKILL.md +208 -0
  121. package/skills/react-feature-arch/references/api-layer.md +110 -0
  122. package/skills/react-feature-arch/references/components.md +192 -0
  123. package/skills/react-feature-arch/references/data-fetching.md +198 -0
  124. package/skills/react-feature-arch/references/forms.md +194 -0
  125. package/skills/react-feature-arch/references/routing.md +148 -0
  126. package/skills/react-feature-arch/references/state-management.md +107 -0
  127. package/skills/tailwind-css/SKILL.md +236 -0
  128. package/skills/tailwind-css/references/components.md +340 -0
  129. package/skills/tailwind-css/references/design-tokens.md +230 -0
  130. package/skills/tailwind-css/references/patterns.md +375 -0
  131. package/skills/tailwind-css/references/setup.md +165 -0
  132. package/skills/zod-validation/SKILL.md +267 -0
@@ -0,0 +1,157 @@
1
+ # Caching Reference — Next.js 15
2
+
3
+ ---
4
+
5
+ ## Caching Layers (top to bottom)
6
+
7
+ | Layer | Scope | How to control |
8
+ |---|---|---|
9
+ | `use cache` | Function / module level | `cacheTag()`, `cacheLife()` |
10
+ | `fetch()` cache | Individual fetch calls | `cache`, `next.revalidate`, `next.tags` |
11
+ | Page-level | Full page / layout | `export const revalidate`, `export const dynamic` |
12
+ | On-demand | After mutations | `revalidateTag()`, `revalidatePath()` |
13
+
14
+ ---
15
+
16
+ ## `use cache` Directive (Next.js 15) — Preferred
17
+
18
+ Tag cached functions for on-demand invalidation:
19
+
20
+ ```ts
21
+ // features/users/users.queries.ts
22
+ 'use cache';
23
+
24
+ import { cacheTag, cacheLife } from 'next/cache';
25
+ import { db } from '@/lib/db';
26
+
27
+ // Cache entire user list, invalidate with 'users' tag
28
+ export async function getUsers() {
29
+ cacheTag('users');
30
+ cacheLife('hours'); // seconds | 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'max'
31
+ return db.user.findMany({ orderBy: { createdAt: 'desc' } });
32
+ }
33
+
34
+ // Per-record cache
35
+ export async function getUserById(id: string) {
36
+ cacheTag('users', `user-${id}`);
37
+ cacheLife('minutes');
38
+ return db.user.findUnique({
39
+ where: { id },
40
+ select: { id: true, name: true, email: true, role: true },
41
+ });
42
+ }
43
+ ```
44
+
45
+ Invalidate from a Server Action or Route Handler:
46
+ ```ts
47
+ import { revalidateTag } from 'next/cache';
48
+
49
+ revalidateTag('users'); // all list caches
50
+ revalidateTag(`user-${id}`); // specific record cache
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Page-Level Caching
56
+
57
+ ```tsx
58
+ // Revalidate the whole page every 60 seconds (ISR)
59
+ export const revalidate = 60;
60
+
61
+ // Always render fresh — opt out of caching entirely
62
+ export const dynamic = 'force-dynamic';
63
+
64
+ // Static generation only — never dynamic
65
+ export const dynamic = 'force-static';
66
+ ```
67
+
68
+ ---
69
+
70
+ ## fetch() Caching
71
+
72
+ ```ts
73
+ // Revalidate every 30 seconds + tag for on-demand invalidation
74
+ const res = await fetch('/api/data', {
75
+ next: { revalidate: 30, tags: ['data'] },
76
+ });
77
+
78
+ // Never cache
79
+ const res = await fetch('/api/live', { cache: 'no-store' });
80
+
81
+ // Cache indefinitely until manually revalidated
82
+ const res = await fetch('/api/config', { cache: 'force-cache' });
83
+ ```
84
+
85
+ ---
86
+
87
+ ## On-Demand Revalidation
88
+
89
+ ```ts
90
+ // In Server Action or Route Handler — called after mutations
91
+ import { revalidateTag, revalidatePath } from 'next/cache';
92
+
93
+ // Prefer revalidateTag (more precise, works with use cache)
94
+ revalidateTag('users');
95
+ revalidateTag(`user-${id}`);
96
+
97
+ // revalidatePath — revalidates all data for a given URL
98
+ revalidatePath('/users');
99
+ revalidatePath('/users/[id]', 'page'); // all dynamic [id] pages
100
+ ```
101
+
102
+ ---
103
+
104
+ ## `after()` — Non-Blocking Post-Response Work
105
+
106
+ Run side effects (emails, analytics, audit logs) after the response is sent to the user:
107
+
108
+ ```ts
109
+ import { after } from 'next/server';
110
+
111
+ export async function createUserAction(formData: FormData) {
112
+ const user = await db.user.create({ /* ... */ });
113
+
114
+ // These run AFTER the redirect/response — user doesn't wait
115
+ after(async () => {
116
+ await sendWelcomeEmail(user.email);
117
+ await logAuditEvent('user.created', { userId: user.id });
118
+ });
119
+
120
+ revalidateTag('users');
121
+ }
122
+ ```
123
+
124
+ ---
125
+
126
+ ## unstable_cache (legacy — prefer `use cache`)
127
+
128
+ ```ts
129
+ import { unstable_cache } from 'next/cache';
130
+
131
+ // Still works in Next.js 15 but use cache directive is preferred
132
+ const getCachedUsers = unstable_cache(
133
+ async () => db.user.findMany(),
134
+ ['users-list'],
135
+ { revalidate: 3600, tags: ['users'] },
136
+ );
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Common Patterns
142
+
143
+ ```ts
144
+ // Static data — config, reference lists (cache forever)
145
+ cacheLife('max');
146
+
147
+ // User content — pages, posts (cache for hours)
148
+ cacheLife('hours');
149
+
150
+ // Dashboard stats (cache for minutes)
151
+ cacheLife('minutes');
152
+
153
+ // Live data — prices, inventory (no cache)
154
+ export const dynamic = 'force-dynamic';
155
+ // or
156
+ const data = await fetch(url, { cache: 'no-store' });
157
+ ```
@@ -0,0 +1,194 @@
1
+ # Route Handlers Reference
2
+
3
+ Use Route Handlers for: public REST APIs consumed by external clients, file uploads, webhooks from third parties, or complex streaming responses. For internal Next.js mutations, prefer Server Actions.
4
+
5
+ ---
6
+
7
+ ## File Conventions
8
+
9
+ ```
10
+ app/api/
11
+ ├── users/
12
+ │ ├── route.ts # GET /api/users, POST /api/users
13
+ │ └── [id]/
14
+ │ └── route.ts # GET, PATCH, DELETE /api/users/:id
15
+ └── webhooks/
16
+ └── stripe/
17
+ └── route.ts # POST /api/webhooks/stripe
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Collection Route — route.ts
23
+
24
+ ```ts
25
+ // app/api/users/route.ts
26
+ import { NextRequest, NextResponse } from 'next/server';
27
+ import { db } from '@/lib/db';
28
+ import { auth } from '@/lib/auth';
29
+ import { z } from 'zod';
30
+
31
+ const listSchema = z.object({
32
+ page: z.coerce.number().int().positive().default(1),
33
+ limit: z.coerce.number().int().positive().max(100).default(10),
34
+ search: z.string().optional(),
35
+ });
36
+
37
+ const createSchema = z.object({
38
+ name: z.string().min(2),
39
+ email: z.string().email(),
40
+ role: z.enum(['USER', 'ADMIN', 'MANAGER']).default('USER'),
41
+ });
42
+
43
+ export async function GET(req: NextRequest) {
44
+ const session = await auth();
45
+ if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
46
+
47
+ const { searchParams } = req.nextUrl;
48
+ const parsed = listSchema.safeParse(Object.fromEntries(searchParams));
49
+
50
+ if (!parsed.success) {
51
+ return NextResponse.json(
52
+ { error: parsed.error.issues[0].message },
53
+ { status: 422 },
54
+ );
55
+ }
56
+
57
+ const { page, limit, search } = parsed.data;
58
+ const skip = (page - 1) * limit;
59
+ const where = search ? { name: { contains: search, mode: 'insensitive' as const } } : {};
60
+
61
+ const [data, total] = await Promise.all([
62
+ db.user.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' } }),
63
+ db.user.count({ where }),
64
+ ]);
65
+
66
+ return NextResponse.json({
67
+ data,
68
+ meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
69
+ });
70
+ }
71
+
72
+ export async function POST(req: NextRequest) {
73
+ const session = await auth();
74
+ if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
75
+
76
+ const body = await req.json();
77
+ const parsed = createSchema.safeParse(body);
78
+
79
+ if (!parsed.success) {
80
+ return NextResponse.json(
81
+ { error: parsed.error.issues[0].message },
82
+ { status: 422 },
83
+ );
84
+ }
85
+
86
+ try {
87
+ const user = await db.user.create({ data: parsed.data });
88
+ return NextResponse.json(user, { status: 201 });
89
+ } catch (err: any) {
90
+ if (err.code === 'P2002') {
91
+ return NextResponse.json({ error: 'Email already registered' }, { status: 409 });
92
+ }
93
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
94
+ }
95
+ }
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Item Route — [id]/route.ts
101
+
102
+ ```ts
103
+ // app/api/users/[id]/route.ts
104
+ import { NextRequest, NextResponse } from 'next/server';
105
+ import { db } from '@/lib/db';
106
+ import { auth } from '@/lib/auth';
107
+ import { z } from 'zod';
108
+
109
+ interface Ctx { params: Promise<{ id: string }> } // Next.js 15 — params is a Promise
110
+
111
+ const updateSchema = z.object({
112
+ name: z.string().min(2).optional(),
113
+ email: z.string().email().optional(),
114
+ }).strict();
115
+
116
+ export async function GET(_req: NextRequest, { params }: Ctx) {
117
+ const { id } = await params;
118
+ const user = await db.user.findUnique({ where: { id } });
119
+ if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 });
120
+ return NextResponse.json(user);
121
+ }
122
+
123
+ export async function PATCH(req: NextRequest, { params }: Ctx) {
124
+ const session = await auth();
125
+ if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
126
+
127
+ const { id } = await params;
128
+ const body = await req.json();
129
+ const parsed = updateSchema.safeParse(body);
130
+
131
+ if (!parsed.success) {
132
+ return NextResponse.json({ error: parsed.error.issues[0].message }, { status: 422 });
133
+ }
134
+
135
+ const user = await db.user.update({ where: { id }, data: parsed.data });
136
+ return NextResponse.json(user);
137
+ }
138
+
139
+ export async function DELETE(_req: NextRequest, { params }: Ctx) {
140
+ const session = await auth();
141
+ if (!session?.user || session.user.role !== 'ADMIN') {
142
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
143
+ }
144
+
145
+ const { id } = await params;
146
+ await db.user.delete({ where: { id } });
147
+ return new NextResponse(null, { status: 204 });
148
+ }
149
+ ```
150
+
151
+ ---
152
+
153
+ ## Webhook Route (raw body, no auth middleware)
154
+
155
+ ```ts
156
+ // app/api/webhooks/stripe/route.ts
157
+ import { NextRequest, NextResponse } from 'next/server';
158
+ import Stripe from 'stripe';
159
+
160
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
161
+
162
+ export async function POST(req: NextRequest) {
163
+ const body = await req.text();
164
+ const signature = req.headers.get('stripe-signature')!;
165
+
166
+ let event: Stripe.Event;
167
+ try {
168
+ event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
169
+ } catch {
170
+ return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
171
+ }
172
+
173
+ switch (event.type) {
174
+ case 'payment_intent.succeeded':
175
+ // handle payment
176
+ break;
177
+ }
178
+
179
+ return NextResponse.json({ received: true });
180
+ }
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Response Helpers
186
+
187
+ ```ts
188
+ // Consistent JSON responses
189
+ return NextResponse.json(data); // 200
190
+ return NextResponse.json(data, { status: 201 }); // 201
191
+ return NextResponse.json({ error: 'msg' }, { status: 400 }); // 400
192
+ return new NextResponse(null, { status: 204 }); // 204 no content
193
+ return NextResponse.redirect(new URL('/login', req.url)); // redirect
194
+ ```
@@ -0,0 +1,214 @@
1
+ # Server Actions Reference
2
+
3
+ Server Actions run on the server. They can be called from Client Components via `action=` prop or event handlers. No separate API route needed for simple mutations.
4
+
5
+ ---
6
+
7
+ ## Action File Pattern — {feature}.actions.ts
8
+
9
+ ```ts
10
+ // features/users/users.actions.ts
11
+ 'use server';
12
+
13
+ import { db } from '@/lib/db';
14
+ import { auth } from '@/lib/auth';
15
+ import { revalidateTag } from 'next/cache';
16
+ import { redirect } from 'next/navigation';
17
+ import { after } from 'next/server';
18
+ import { createUserSchema, updateUserSchema } from './users.schema';
19
+
20
+ // Create — returns state object for useActionState
21
+ export async function createUserAction(_prev: unknown, formData: FormData) {
22
+ const session = await auth();
23
+ if (!session) return { error: 'Unauthorized' };
24
+
25
+ const result = createUserSchema.safeParse({
26
+ name: formData.get('name'),
27
+ email: formData.get('email'),
28
+ role: formData.get('role'),
29
+ });
30
+
31
+ if (!result.success) {
32
+ return { error: result.error.issues[0].message };
33
+ }
34
+
35
+ try {
36
+ const user = await db.user.create({ data: result.data });
37
+ after(() => sendWelcomeEmail(user.email)); // fire-and-forget after response
38
+ } catch (err: any) {
39
+ if (err.code === 'P2002') return { error: 'Email already registered' };
40
+ return { error: 'Failed to create user' };
41
+ }
42
+
43
+ revalidateTag('users');
44
+ redirect('/users');
45
+ }
46
+
47
+ // Update — accepts structured args (not FormData)
48
+ export async function updateUserAction(id: string, data: { name?: string; email?: string }) {
49
+ const session = await auth();
50
+ if (!session) throw new Error('Unauthorized');
51
+
52
+ const result = updateUserSchema.safeParse(data);
53
+ if (!result.success) throw new Error(result.error.issues[0].message);
54
+
55
+ await db.user.update({ where: { id }, data: result.data });
56
+
57
+ revalidateTag('users');
58
+ revalidateTag(`user-${id}`);
59
+ }
60
+
61
+ // Delete — called from event handler in Client Component
62
+ export async function deleteUserAction(id: string) {
63
+ const session = await auth();
64
+ if (!session?.user || session.user.role !== 'ADMIN') {
65
+ throw new Error('Forbidden');
66
+ }
67
+
68
+ await db.user.delete({ where: { id } });
69
+
70
+ revalidateTag('users');
71
+ revalidateTag(`user-${id}`);
72
+ }
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Client Form — useActionState (React 19)
78
+
79
+ `useActionState` is the primary way to wire a Server Action to a form with pending + error state:
80
+
81
+ ```tsx
82
+ // features/users/components/CreateUserForm.tsx
83
+ 'use client';
84
+
85
+ import { useActionState } from 'react';
86
+ import { createUserAction } from '../users.actions';
87
+
88
+ export function CreateUserForm() {
89
+ const [state, formAction, isPending] = useActionState(createUserAction, null);
90
+
91
+ return (
92
+ <form action={formAction}>
93
+ <div>
94
+ <label>Name</label>
95
+ <input name="name" required />
96
+ </div>
97
+ <div>
98
+ <label>Email</label>
99
+ <input name="email" type="email" required />
100
+ </div>
101
+ <select name="role" defaultValue="USER">
102
+ <option value="USER">User</option>
103
+ <option value="ADMIN">Admin</option>
104
+ </select>
105
+
106
+ {state?.error && <p className="error">{state.error}</p>}
107
+
108
+ <button type="submit" disabled={isPending}>
109
+ {isPending ? 'Creating...' : 'Create User'}
110
+ </button>
111
+ </form>
112
+ );
113
+ }
114
+ ```
115
+
116
+ ---
117
+
118
+ ## useOptimistic — Optimistic Updates
119
+
120
+ ```tsx
121
+ 'use client';
122
+
123
+ import { useOptimistic, useTransition } from 'react';
124
+ import { deleteUserAction } from '../users.actions';
125
+ import type { User } from '../users.schema';
126
+
127
+ export function UserList({ users }: { users: User[] }) {
128
+ const [optimisticUsers, removeOptimistic] = useOptimistic(
129
+ users,
130
+ (state, removedId: string) => state.filter((u) => u.id !== removedId),
131
+ );
132
+ const [isPending, startTransition] = useTransition();
133
+
134
+ const handleDelete = (id: string) => {
135
+ startTransition(async () => {
136
+ removeOptimistic(id); // instant UI update
137
+ await deleteUserAction(id); // real delete on server
138
+ });
139
+ };
140
+
141
+ return (
142
+ <ul>
143
+ {optimisticUsers.map((user) => (
144
+ <li key={user.id}>
145
+ {user.name}
146
+ <button onClick={() => handleDelete(user.id)} disabled={isPending}>
147
+ Delete
148
+ </button>
149
+ </li>
150
+ ))}
151
+ </ul>
152
+ );
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Calling Actions from Event Handlers
159
+
160
+ Actions can also be called directly (not through a form `action=`):
161
+
162
+ ```tsx
163
+ 'use client';
164
+
165
+ import { useTransition } from 'react';
166
+ import { updateUserAction } from '../users.actions';
167
+
168
+ export function StatusToggle({ user }: { user: User }) {
169
+ const [isPending, startTransition] = useTransition();
170
+
171
+ const toggle = () => {
172
+ startTransition(async () => {
173
+ await updateUserAction(user.id, { active: !user.active });
174
+ });
175
+ };
176
+
177
+ return (
178
+ <button onClick={toggle} disabled={isPending}>
179
+ {user.active ? 'Deactivate' : 'Activate'}
180
+ </button>
181
+ );
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ## useFormStatus — Pending State in Child Components
188
+
189
+ ```tsx
190
+ 'use client';
191
+
192
+ import { useFormStatus } from 'react-dom';
193
+
194
+ // Reads pending state from the nearest parent <form>
195
+ export function SubmitButton({ label }: { label: string }) {
196
+ const { pending } = useFormStatus();
197
+
198
+ return (
199
+ <button type="submit" disabled={pending}>
200
+ {pending ? 'Saving...' : label}
201
+ </button>
202
+ );
203
+ }
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Rules
209
+
210
+ - Always authenticate at the top of every Server Action — never trust the caller
211
+ - Return `{ error: string }` from `useActionState` actions (never throw — it breaks the state pattern)
212
+ - Throw from actions called via `startTransition` — error boundary catches it
213
+ - Use `revalidateTag` after mutations, never `revalidatePath` for cacheable data
214
+ - Use `after()` for non-critical side effects (emails, logs) — keeps the action fast