@caddis/cli 0.0.0 → 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.
- package/LICENSE +21 -0
- package/README.md +150 -1
- package/bundles/antigravity-plugin/agents/code-reviewer.md +47 -0
- package/bundles/antigravity-plugin/agents/preflight.md +53 -0
- package/bundles/antigravity-plugin/guard_agy.py +338 -0
- package/bundles/antigravity-plugin/hooks.json +36 -0
- package/bundles/antigravity-plugin/mcp_config.json +8 -0
- package/bundles/antigravity-plugin/mcp_ping_server.py +55 -0
- package/bundles/antigravity-plugin/plugin.json +5 -0
- package/bundles/antigravity-plugin/session_end_agy.py +57 -0
- package/bundles/antigravity-plugin/skills/_registry.md +115 -0
- package/bundles/antigravity-plugin/skills/add-rules/SKILL.md +45 -0
- package/bundles/antigravity-plugin/skills/api-design/SKILL.md +249 -0
- package/bundles/antigravity-plugin/skills/backend-development/SKILL.md +305 -0
- package/bundles/antigravity-plugin/skills/best-practices/SKILL.md +500 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/best-practices-referencer.md +263 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/codebase-context-builder.md +326 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/task-intent-analyzer.md +245 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/anti-patterns.md +571 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/before-after-examples.md +1114 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/best-practices-guide.md +513 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/common-workflows.md +692 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/prompt-patterns.md +547 -0
- package/bundles/antigravity-plugin/skills/brainstorming/SKILL.md +57 -0
- package/bundles/antigravity-plugin/skills/ci-cd-pipeline/SKILL.md +315 -0
- package/bundles/antigravity-plugin/skills/code-documentation/SKILL.md +271 -0
- package/bundles/antigravity-plugin/skills/code-review/SKILL.md +122 -0
- package/bundles/antigravity-plugin/skills/codebase-audit/SKILL.md +204 -0
- package/bundles/antigravity-plugin/skills/context-curator/SKILL.md +157 -0
- package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +40 -0
- package/bundles/antigravity-plugin/skills/css-architecture/SKILL.md +305 -0
- package/bundles/antigravity-plugin/skills/css-architecture/references/RESPONSIVE-DESIGN.md +604 -0
- package/bundles/antigravity-plugin/skills/database-design/SKILL.md +177 -0
- package/bundles/antigravity-plugin/skills/db-diagram/SKILL.md +148 -0
- package/bundles/antigravity-plugin/skills/db-diagram/scripts/sql_to_graph.py +1212 -0
- package/bundles/antigravity-plugin/skills/digress/SKILL.md +61 -0
- package/bundles/antigravity-plugin/skills/draw-io/SKILL.md +162 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/aws-icons.md +677 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/layout-guidelines.md +142 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/troubleshooting.md +118 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/workflows.md +103 -0
- package/bundles/antigravity-plugin/skills/draw-io/scripts/convert-drawio-to-png.sh +25 -0
- package/bundles/antigravity-plugin/skills/draw-io/scripts/find_aws_icon.py +79 -0
- package/bundles/antigravity-plugin/skills/error-handling/SKILL.md +260 -0
- package/bundles/antigravity-plugin/skills/excalidraw-db/SKILL.md +38 -0
- package/bundles/antigravity-plugin/skills/fastapi-dev/SKILL.md +300 -0
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +198 -0
- package/bundles/antigravity-plugin/skills/frontend-design/SKILL.md +193 -0
- package/bundles/antigravity-plugin/skills/frontend-design/references/my-tech-stack.md +127 -0
- package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +195 -0
- package/bundles/antigravity-plugin/skills/git-commit/SKILL.md +285 -0
- package/bundles/antigravity-plugin/skills/golden-plan/SKILL.md +577 -0
- package/bundles/antigravity-plugin/skills/handoff/SKILL.md +100 -0
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +114 -0
- package/bundles/antigravity-plugin/skills/javascript-typescript/SKILL.md +142 -0
- package/bundles/antigravity-plugin/skills/kb/SKILL.md +60 -0
- package/bundles/antigravity-plugin/skills/mermaid-db/SKILL.md +33 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/SKILL.md +236 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/ENHANCEMENTS.md +264 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/MERMAID-SUMMARY.md +137 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/advanced-features.md +556 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/architecture-diagrams.md +192 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/c4-diagrams.md +410 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/class-diagrams.md +361 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/erd-diagrams.md +510 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/flowcharts.md +450 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/sequence-diagrams.md +394 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/troubleshooting.md +335 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/workflows.md +418 -0
- package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +68 -0
- package/bundles/antigravity-plugin/skills/mockup/SKILL.md +242 -0
- package/bundles/antigravity-plugin/skills/particle-art/SKILL.md +243 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/canvas-utils.ts +171 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/dot-field.template.tsx +203 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/flow-field.template.tsx +263 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/node-shape.template.tsx +261 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/shape-sampler.ts +281 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/stipple-morph.template.tsx +167 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/stipple.template.tsx +175 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/trail-ghost.template.tsx +266 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/usage-examples.md +320 -0
- package/bundles/antigravity-plugin/skills/playwright/API_REFERENCE.md +653 -0
- package/bundles/antigravity-plugin/skills/playwright/SKILL.md +454 -0
- package/bundles/antigravity-plugin/skills/playwright/lib/helpers.js +441 -0
- package/bundles/antigravity-plugin/skills/playwright/package.json +26 -0
- package/bundles/antigravity-plugin/skills/playwright/run.js +228 -0
- package/bundles/antigravity-plugin/skills/prd/SKILL.md +107 -0
- package/bundles/antigravity-plugin/skills/preflight/SKILL.md +435 -0
- package/bundles/antigravity-plugin/skills/python/SKILL.md +388 -0
- package/bundles/antigravity-plugin/skills/react-best-practices/SKILL.md +269 -0
- package/bundles/antigravity-plugin/skills/react-dev/README.md +404 -0
- package/bundles/antigravity-plugin/skills/react-dev/SKILL.md +459 -0
- package/bundles/antigravity-plugin/skills/react-dev/examples/generic-components.md +579 -0
- package/bundles/antigravity-plugin/skills/react-dev/examples/server-components.md +579 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/event-handlers.md +574 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/hooks.md +456 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/react-19-patterns.md +638 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/react-router.md +1002 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/tanstack-router.md +587 -0
- package/bundles/antigravity-plugin/skills/refactoring/SKILL.md +486 -0
- package/bundles/antigravity-plugin/skills/resume/SKILL.md +36 -0
- package/bundles/antigravity-plugin/skills/security-review/SKILL.md +196 -0
- package/bundles/antigravity-plugin/skills/setup-project-ai/SKILL.md +61 -0
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +107 -0
- package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +103 -0
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +102 -0
- package/bundles/antigravity-plugin/skills/skill-creator/LICENSE.txt +202 -0
- package/bundles/antigravity-plugin/skills/skill-creator/SKILL.md +491 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/analyzer.md +274 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/comparator.md +202 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/grader.md +223 -0
- package/bundles/antigravity-plugin/skills/skill-creator/assets/eval_review.html +146 -0
- package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/bundles/antigravity-plugin/skills/skill-creator/references/schemas.md +430 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/__init__.py +0 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/generate_report.py +326 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/improve_description.py +247 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/package_skill.py +136 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_eval.py +310 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_loop.py +328 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/utils.py +47 -0
- package/bundles/antigravity-plugin/skills/sql/SKILL.md +321 -0
- package/bundles/antigravity-plugin/skills/tdd/SKILL.md +37 -0
- package/bundles/antigravity-plugin/skills/tdd-workflow/SKILL.md +188 -0
- package/bundles/antigravity-plugin/skills/technical-writing/SKILL.md +286 -0
- package/bundles/antigravity-plugin/skills/test-strategy/SKILL.md +155 -0
- package/bundles/antigravity-plugin/skills/ui-brief/SKILL.md +84 -0
- package/bundles/antigravity-plugin/skills/ui-review/SKILL.md +176 -0
- package/bundles/antigravity-plugin/skills/ui-review/references/framework-fixes.md +471 -0
- package/bundles/antigravity-plugin/skills/ui-review/references/visual-checklist.md +236 -0
- package/bundles/antigravity-plugin/skills/usage-review/SKILL.md +77 -0
- package/bundles/antigravity-plugin/skills/use-model/SKILL.md +64 -0
- package/bundles/antigravity-plugin/skills/using-git-worktrees/SKILL.md +217 -0
- package/bundles/antigravity-plugin/skills/version/SKILL.md +18 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/DESIGN_TOKENS.md +487 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/IMPLEMENTATION_GUIDE.md +177 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/SKILL.md +732 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/LICENSE.txt +202 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/SKILL.md +97 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/console_logging.py +35 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/scripts/with_server.py +106 -0
- package/bundles/antigravity-plugin/skills/windows-deployment/SKILL.md +880 -0
- package/bundles/antigravity-plugin/skills/writing-plans/SKILL.md +384 -0
- package/bundles/antigravity-plugin/statusline-command-agy.sh +91 -0
- package/bundles/antigravity-plugin/warm_start_agy.py +149 -0
- package/bundles/manifest.json +6 -0
- package/dist/cli.js +5363 -0
- package/package.json +61 -4
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
# Server Components and Server Actions
|
|
2
|
+
|
|
3
|
+
React 19 Server Components run on server, can be async, enable zero-bundle data fetching. Server Actions handle mutations with progressive enhancement.
|
|
4
|
+
|
|
5
|
+
## Async Server Component
|
|
6
|
+
|
|
7
|
+
Server Components can be async functions — await data fetching directly.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// app/users/[id]/page.tsx
|
|
11
|
+
type PageProps = {
|
|
12
|
+
params: { id: string };
|
|
13
|
+
searchParams?: { tab?: string; edit?: string };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default async function UserPage({ params, searchParams }: PageProps) {
|
|
17
|
+
// Runs on server - no client bundle
|
|
18
|
+
const user = await fetchUser(params.id);
|
|
19
|
+
const posts = await fetchUserPosts(params.id);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div>
|
|
23
|
+
<header>
|
|
24
|
+
<h1>{user.name}</h1>
|
|
25
|
+
<p>{user.email}</p>
|
|
26
|
+
</header>
|
|
27
|
+
|
|
28
|
+
<UserTabs user={user} posts={posts} activeTab={searchParams?.tab} />
|
|
29
|
+
|
|
30
|
+
{searchParams?.edit === 'true' && (
|
|
31
|
+
<UserEditForm user={user} />
|
|
32
|
+
)}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function fetchUser(id: string): Promise<User> {
|
|
38
|
+
const res = await fetch(`https://api.example.com/users/${id}`, {
|
|
39
|
+
cache: 'no-store', // Or 'force-cache', 'revalidate'
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok) throw new Error('Failed to fetch user');
|
|
42
|
+
return res.json();
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Parallel Data Fetching
|
|
47
|
+
|
|
48
|
+
Fetch multiple resources in parallel with Promise.all.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
type DashboardProps = {
|
|
52
|
+
params: { userId: string };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default async function Dashboard({ params }: DashboardProps) {
|
|
56
|
+
// Parallel fetching
|
|
57
|
+
const [user, stats, activity] = await Promise.all([
|
|
58
|
+
fetchUser(params.userId),
|
|
59
|
+
fetchUserStats(params.userId),
|
|
60
|
+
fetchRecentActivity(params.userId),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div>
|
|
65
|
+
<UserHeader user={user} />
|
|
66
|
+
<StatsGrid stats={stats} />
|
|
67
|
+
<ActivityFeed items={activity} />
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Sequential vs Waterfall Fetching
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// ❌ Waterfall - slow
|
|
77
|
+
async function SlowPage() {
|
|
78
|
+
const user = await fetchUser('123');
|
|
79
|
+
const posts = await fetchUserPosts(user.id); // Waits for user
|
|
80
|
+
const comments = await fetchPostComments(posts[0].id); // Waits for posts
|
|
81
|
+
return <div>...</div>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ✅ Parallel - fast
|
|
85
|
+
async function FastPage() {
|
|
86
|
+
const userPromise = fetchUser('123');
|
|
87
|
+
const postsPromise = fetchUserPosts('123');
|
|
88
|
+
|
|
89
|
+
const [user, posts] = await Promise.all([userPromise, postsPromise]);
|
|
90
|
+
|
|
91
|
+
// If comments depend on posts, fetch after
|
|
92
|
+
const comments = await fetchPostComments(posts[0].id);
|
|
93
|
+
|
|
94
|
+
return <div>...</div>;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Server Actions - Form Mutations
|
|
99
|
+
|
|
100
|
+
Server Actions marked with 'use server' — run on server, callable from client.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// actions/user.ts
|
|
104
|
+
'use server';
|
|
105
|
+
|
|
106
|
+
import { revalidatePath, revalidateTag } from 'next/cache';
|
|
107
|
+
import { redirect } from 'next/navigation';
|
|
108
|
+
import { z } from 'zod';
|
|
109
|
+
|
|
110
|
+
const updateUserSchema = z.object({
|
|
111
|
+
name: z.string().min(2, 'Name must be at least 2 characters'),
|
|
112
|
+
email: z.string().email('Invalid email address'),
|
|
113
|
+
bio: z.string().max(500, 'Bio must be less than 500 characters').optional(),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
type FormState = {
|
|
117
|
+
success?: boolean;
|
|
118
|
+
errors?: Record<string, string[]>;
|
|
119
|
+
message?: string;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export async function updateUser(
|
|
123
|
+
userId: string,
|
|
124
|
+
prevState: FormState,
|
|
125
|
+
formData: FormData
|
|
126
|
+
): Promise<FormState> {
|
|
127
|
+
// Validate
|
|
128
|
+
const parsed = updateUserSchema.safeParse({
|
|
129
|
+
name: formData.get('name'),
|
|
130
|
+
email: formData.get('email'),
|
|
131
|
+
bio: formData.get('bio'),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!parsed.success) {
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
errors: parsed.error.flatten().fieldErrors,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Mutate database
|
|
143
|
+
await db.user.update({
|
|
144
|
+
where: { id: userId },
|
|
145
|
+
data: parsed.data,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Revalidate cached data
|
|
149
|
+
revalidatePath(`/users/${userId}`);
|
|
150
|
+
revalidateTag(`user-${userId}`);
|
|
151
|
+
|
|
152
|
+
return { success: true, message: 'Profile updated successfully' };
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
message: 'Failed to update profile. Please try again.',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function deleteUser(userId: string) {
|
|
162
|
+
await db.user.delete({ where: { id: userId } });
|
|
163
|
+
revalidatePath('/users');
|
|
164
|
+
redirect('/users'); // Navigate after mutation
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Client Component Using Server Action
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// components/UserForm.tsx
|
|
172
|
+
'use client';
|
|
173
|
+
|
|
174
|
+
import { useActionState } from 'react';
|
|
175
|
+
import { updateUser } from '@/actions/user';
|
|
176
|
+
|
|
177
|
+
type FormState = {
|
|
178
|
+
success?: boolean;
|
|
179
|
+
errors?: Record<string, string[]>;
|
|
180
|
+
message?: string;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export function UserEditForm({ userId, initialData }: Props) {
|
|
184
|
+
const [state, formAction, isPending] = useActionState<FormState, FormData>(
|
|
185
|
+
(prevState, formData) => updateUser(userId, prevState, formData),
|
|
186
|
+
{}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<form action={formAction}>
|
|
191
|
+
<div>
|
|
192
|
+
<label htmlFor="name">Name</label>
|
|
193
|
+
<input
|
|
194
|
+
id="name"
|
|
195
|
+
name="name"
|
|
196
|
+
defaultValue={initialData.name}
|
|
197
|
+
required
|
|
198
|
+
aria-invalid={!!state.errors?.name}
|
|
199
|
+
/>
|
|
200
|
+
{state.errors?.name?.map((error) => (
|
|
201
|
+
<p key={error} className="error">
|
|
202
|
+
{error}
|
|
203
|
+
</p>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div>
|
|
208
|
+
<label htmlFor="email">Email</label>
|
|
209
|
+
<input
|
|
210
|
+
id="email"
|
|
211
|
+
name="email"
|
|
212
|
+
type="email"
|
|
213
|
+
defaultValue={initialData.email}
|
|
214
|
+
required
|
|
215
|
+
aria-invalid={!!state.errors?.email}
|
|
216
|
+
/>
|
|
217
|
+
{state.errors?.email?.map((error) => (
|
|
218
|
+
<p key={error} className="error">
|
|
219
|
+
{error}
|
|
220
|
+
</p>
|
|
221
|
+
))}
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div>
|
|
225
|
+
<label htmlFor="bio">Bio</label>
|
|
226
|
+
<textarea
|
|
227
|
+
id="bio"
|
|
228
|
+
name="bio"
|
|
229
|
+
defaultValue={initialData.bio}
|
|
230
|
+
aria-invalid={!!state.errors?.bio}
|
|
231
|
+
/>
|
|
232
|
+
{state.errors?.bio?.map((error) => (
|
|
233
|
+
<p key={error} className="error">
|
|
234
|
+
{error}
|
|
235
|
+
</p>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{state.message && (
|
|
240
|
+
<div className={state.success ? 'success' : 'error'}>{state.message}</div>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
<button type="submit" disabled={isPending}>
|
|
244
|
+
{isPending ? 'Saving...' : 'Save Changes'}
|
|
245
|
+
</button>
|
|
246
|
+
</form>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Programmatic Server Action
|
|
252
|
+
|
|
253
|
+
Call Server Actions directly from client code, not just forms.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
'use client';
|
|
257
|
+
|
|
258
|
+
import { deleteUser } from '@/actions/user';
|
|
259
|
+
import { useTransition } from 'react';
|
|
260
|
+
|
|
261
|
+
export function DeleteButton({ userId }: { userId: string }) {
|
|
262
|
+
const [isPending, startTransition] = useTransition();
|
|
263
|
+
|
|
264
|
+
const handleDelete = () => {
|
|
265
|
+
if (!confirm('Are you sure you want to delete this user?')) return;
|
|
266
|
+
|
|
267
|
+
startTransition(async () => {
|
|
268
|
+
try {
|
|
269
|
+
await deleteUser(userId);
|
|
270
|
+
// deleteUser calls redirect(), navigation happens automatically
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error('Failed to delete user:', error);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<button onClick={handleDelete} disabled={isPending}>
|
|
279
|
+
{isPending ? 'Deleting...' : 'Delete User'}
|
|
280
|
+
</button>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## use() Hook - Unwrapping Promises
|
|
286
|
+
|
|
287
|
+
Pass promises from Server to Client components, unwrap with use().
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// Server Component
|
|
291
|
+
async function UserPage({ params }: { params: { id: string } }) {
|
|
292
|
+
// Don't await - pass promise to client
|
|
293
|
+
const userPromise = fetchUser(params.id);
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<Suspense fallback={<UserSkeleton />}>
|
|
297
|
+
<UserProfile userPromise={userPromise} />
|
|
298
|
+
</Suspense>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Client Component
|
|
303
|
+
'use client';
|
|
304
|
+
|
|
305
|
+
import { use } from 'react';
|
|
306
|
+
|
|
307
|
+
type Props = {
|
|
308
|
+
userPromise: Promise<User>;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export function UserProfile({ userPromise }: Props) {
|
|
312
|
+
// Suspends until resolved
|
|
313
|
+
const user = use(userPromise);
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<div>
|
|
317
|
+
<h1>{user.name}</h1>
|
|
318
|
+
<p>{user.email}</p>
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## use() with Context
|
|
325
|
+
|
|
326
|
+
use() also unwraps context — alternative to useContext.
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
'use client';
|
|
330
|
+
|
|
331
|
+
import { use } from 'react';
|
|
332
|
+
import { ThemeContext } from './ThemeProvider';
|
|
333
|
+
|
|
334
|
+
export function ThemedButton() {
|
|
335
|
+
const theme = use(ThemeContext); // Same as useContext(ThemeContext)
|
|
336
|
+
|
|
337
|
+
return <button className={theme.mode}>{theme.primaryColor}</button>;
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Streaming with Suspense
|
|
342
|
+
|
|
343
|
+
Stream components as they resolve — faster initial page load.
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
// Server Component
|
|
347
|
+
export default async function Page() {
|
|
348
|
+
return (
|
|
349
|
+
<div>
|
|
350
|
+
<h1>Dashboard</h1>
|
|
351
|
+
|
|
352
|
+
{/* Renders immediately */}
|
|
353
|
+
<StaticContent />
|
|
354
|
+
|
|
355
|
+
{/* Streams when ready */}
|
|
356
|
+
<Suspense fallback={<Spinner />}>
|
|
357
|
+
<SlowComponent />
|
|
358
|
+
</Suspense>
|
|
359
|
+
|
|
360
|
+
{/* Independent stream */}
|
|
361
|
+
<Suspense fallback={<Skeleton />}>
|
|
362
|
+
<AnotherSlowComponent />
|
|
363
|
+
</Suspense>
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function SlowComponent() {
|
|
369
|
+
const data = await slowFetch(); // Takes 2s
|
|
370
|
+
return <div>{data}</div>;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function AnotherSlowComponent() {
|
|
374
|
+
const data = await anotherSlowFetch(); // Takes 1s
|
|
375
|
+
return <div>{data}</div>;
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Error Handling in Server Components
|
|
380
|
+
|
|
381
|
+
Use error.tsx for error boundaries.
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// app/users/[id]/error.tsx
|
|
385
|
+
'use client';
|
|
386
|
+
|
|
387
|
+
export default function Error({
|
|
388
|
+
error,
|
|
389
|
+
reset,
|
|
390
|
+
}: {
|
|
391
|
+
error: Error & { digest?: string };
|
|
392
|
+
reset: () => void;
|
|
393
|
+
}) {
|
|
394
|
+
return (
|
|
395
|
+
<div>
|
|
396
|
+
<h2>Something went wrong!</h2>
|
|
397
|
+
<p>{error.message}</p>
|
|
398
|
+
<button onClick={reset}>Try again</button>
|
|
399
|
+
</div>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Server Component throws error
|
|
404
|
+
export default async function UserPage({ params }: Props) {
|
|
405
|
+
const user = await fetchUser(params.id);
|
|
406
|
+
|
|
407
|
+
if (!user) {
|
|
408
|
+
throw new Error('User not found'); // Caught by error.tsx
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return <div>{user.name}</div>;
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## Loading States with loading.tsx
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// app/users/[id]/loading.tsx
|
|
419
|
+
export default function Loading() {
|
|
420
|
+
return <UserSkeleton />;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Automatically wraps page in Suspense
|
|
424
|
+
// No need for manual Suspense boundary
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Server-Only Code
|
|
428
|
+
|
|
429
|
+
Ensure code never runs on client.
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
// lib/server-only-utils.ts
|
|
433
|
+
import 'server-only'; // Throws if imported in client component
|
|
434
|
+
|
|
435
|
+
export async function getSecretKey() {
|
|
436
|
+
return process.env.SECRET_KEY; // Safe - never in client bundle
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export async function hashPassword(password: string) {
|
|
440
|
+
const bcrypt = await import('bcrypt');
|
|
441
|
+
return bcrypt.hash(password, 10);
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Client-Only Code
|
|
446
|
+
|
|
447
|
+
Ensure code never runs on server.
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
// lib/client-only-utils.ts
|
|
451
|
+
import 'client-only';
|
|
452
|
+
|
|
453
|
+
export function useLocalStorage(key: string) {
|
|
454
|
+
// localStorage only available in browser
|
|
455
|
+
const [value, setValue] = useState(() => localStorage.getItem(key));
|
|
456
|
+
|
|
457
|
+
useEffect(() => {
|
|
458
|
+
localStorage.setItem(key, value || '');
|
|
459
|
+
}, [key, value]);
|
|
460
|
+
|
|
461
|
+
return [value, setValue] as const;
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Mixing Server and Client Components
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
// app/page.tsx (Server Component)
|
|
469
|
+
export default async function Page() {
|
|
470
|
+
const data = await fetchData();
|
|
471
|
+
|
|
472
|
+
return (
|
|
473
|
+
<div>
|
|
474
|
+
{/* Server Component - can be async */}
|
|
475
|
+
<ServerComponent data={data} />
|
|
476
|
+
|
|
477
|
+
{/* Client Component - interactive */}
|
|
478
|
+
<ClientComponent initialData={data} />
|
|
479
|
+
</div>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ServerComponent.tsx (Server Component - default)
|
|
484
|
+
export function ServerComponent({ data }: { data: Data }) {
|
|
485
|
+
return <div>{data.title}</div>;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ClientComponent.tsx (Client Component)
|
|
489
|
+
'use client';
|
|
490
|
+
|
|
491
|
+
export function ClientComponent({ initialData }: { initialData: Data }) {
|
|
492
|
+
const [data, setData] = useState(initialData);
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
<button onClick={() => setData({ ...data, count: data.count + 1 })}>
|
|
496
|
+
{data.count}
|
|
497
|
+
</button>
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## Server Component Patterns
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
// ✅ Server Component can:
|
|
506
|
+
// - Be async
|
|
507
|
+
// - Fetch data directly
|
|
508
|
+
// - Access backend resources (DB, filesystem)
|
|
509
|
+
// - Use server-only packages
|
|
510
|
+
// - Pass serializable props to client components
|
|
511
|
+
|
|
512
|
+
export default async function Page() {
|
|
513
|
+
const db = await connectDB(); // Direct DB access
|
|
514
|
+
const users = await db.user.findMany();
|
|
515
|
+
|
|
516
|
+
return <UserList users={users} />; // Pass serializable data
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ❌ Server Component cannot:
|
|
520
|
+
// - Use hooks (useState, useEffect, etc)
|
|
521
|
+
// - Use browser APIs (localStorage, window, etc)
|
|
522
|
+
// - Add event listeners (onClick, onChange, etc)
|
|
523
|
+
// - Use React Context
|
|
524
|
+
|
|
525
|
+
// ✅ Client Component can:
|
|
526
|
+
// - Use hooks
|
|
527
|
+
// - Use browser APIs
|
|
528
|
+
// - Add event listeners
|
|
529
|
+
// - Use React Context
|
|
530
|
+
// - Import Server Components as children
|
|
531
|
+
|
|
532
|
+
'use client';
|
|
533
|
+
|
|
534
|
+
export function Layout({ children }: { children: React.ReactNode }) {
|
|
535
|
+
const [theme, setTheme] = useState('light');
|
|
536
|
+
|
|
537
|
+
return (
|
|
538
|
+
<div className={theme}>
|
|
539
|
+
<button onClick={() => setTheme('dark')}>Toggle</button>
|
|
540
|
+
{children} {/* Server Component can be child */}
|
|
541
|
+
</div>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ❌ Client Component cannot:
|
|
546
|
+
// - Be async
|
|
547
|
+
// - Directly access backend resources
|
|
548
|
+
// - Import server-only packages
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
## Progressive Enhancement with Server Actions
|
|
552
|
+
|
|
553
|
+
Forms work without JavaScript when using Server Actions.
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
// components/AddTodoForm.tsx
|
|
557
|
+
import { addTodo } from '@/actions/todos';
|
|
558
|
+
|
|
559
|
+
export function AddTodoForm() {
|
|
560
|
+
return (
|
|
561
|
+
<form action={addTodo}>
|
|
562
|
+
<input name="title" required />
|
|
563
|
+
<button type="submit">Add Todo</button>
|
|
564
|
+
{/* Works without JS - progressive enhancement */}
|
|
565
|
+
</form>
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// actions/todos.ts
|
|
570
|
+
'use server';
|
|
571
|
+
|
|
572
|
+
export async function addTodo(formData: FormData) {
|
|
573
|
+
const title = formData.get('title');
|
|
574
|
+
if (typeof title !== 'string') return;
|
|
575
|
+
|
|
576
|
+
await db.todo.create({ data: { title } });
|
|
577
|
+
revalidatePath('/todos');
|
|
578
|
+
}
|
|
579
|
+
```
|