@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,638 @@
|
|
|
1
|
+
# React 19 TypeScript Patterns
|
|
2
|
+
|
|
3
|
+
React 19 introduces breaking changes and new APIs requiring updated TypeScript patterns.
|
|
4
|
+
|
|
5
|
+
## ref as Prop (No More forwardRef)
|
|
6
|
+
|
|
7
|
+
React 19 allows ref as regular prop — forwardRef deprecated but still works.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// ✅ React 19 - ref as prop
|
|
11
|
+
type InputProps = {
|
|
12
|
+
ref?: React.Ref<HTMLInputElement>;
|
|
13
|
+
label: string;
|
|
14
|
+
} & React.ComponentPropsWithoutRef<'input'>;
|
|
15
|
+
|
|
16
|
+
export function Input({ ref, label, ...props }: InputProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div>
|
|
19
|
+
<label>{label}</label>
|
|
20
|
+
<input ref={ref} {...props} />
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Usage
|
|
26
|
+
function Form() {
|
|
27
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<form>
|
|
31
|
+
<Input ref={inputRef} label="Name" />
|
|
32
|
+
<button onClick={() => inputRef.current?.focus()}>Focus</button>
|
|
33
|
+
</form>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// ❌ Old pattern (still works, but unnecessary)
|
|
40
|
+
import { forwardRef } from 'react';
|
|
41
|
+
|
|
42
|
+
type InputProps = {
|
|
43
|
+
label: string;
|
|
44
|
+
} & React.ComponentPropsWithoutRef<'input'>;
|
|
45
|
+
|
|
46
|
+
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
47
|
+
({ label, ...props }, ref) => {
|
|
48
|
+
return (
|
|
49
|
+
<div>
|
|
50
|
+
<label>{label}</label>
|
|
51
|
+
<input ref={ref} {...props} />
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
Input.displayName = 'Input';
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Generic Components with ref
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
type SelectProps<T> = {
|
|
64
|
+
ref?: React.Ref<HTMLSelectElement>;
|
|
65
|
+
options: T[];
|
|
66
|
+
value: T;
|
|
67
|
+
onChange: (value: T) => void;
|
|
68
|
+
getLabel: (option: T) => string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function Select<T>({ ref, options, value, onChange, getLabel }: SelectProps<T>) {
|
|
72
|
+
return (
|
|
73
|
+
<select
|
|
74
|
+
ref={ref}
|
|
75
|
+
value={getLabel(value)}
|
|
76
|
+
onChange={(e) => {
|
|
77
|
+
const selected = options.find((opt) => getLabel(opt) === e.target.value);
|
|
78
|
+
if (selected) onChange(selected);
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{options.map((opt) => (
|
|
82
|
+
<option key={getLabel(opt)} value={getLabel(opt)}>
|
|
83
|
+
{getLabel(opt)}
|
|
84
|
+
</option>
|
|
85
|
+
))}
|
|
86
|
+
</select>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Combining ref with Other Props
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
type ButtonProps = {
|
|
95
|
+
ref?: React.Ref<HTMLButtonElement>;
|
|
96
|
+
variant?: 'primary' | 'secondary';
|
|
97
|
+
size?: 'sm' | 'md' | 'lg';
|
|
98
|
+
} & React.ComponentPropsWithoutRef<'button'>;
|
|
99
|
+
|
|
100
|
+
export function Button({
|
|
101
|
+
ref,
|
|
102
|
+
variant = 'primary',
|
|
103
|
+
size = 'md',
|
|
104
|
+
className,
|
|
105
|
+
children,
|
|
106
|
+
...props
|
|
107
|
+
}: ButtonProps) {
|
|
108
|
+
return (
|
|
109
|
+
<button
|
|
110
|
+
ref={ref}
|
|
111
|
+
className={`btn btn-${variant} btn-${size} ${className || ''}`}
|
|
112
|
+
{...props}
|
|
113
|
+
>
|
|
114
|
+
{children}
|
|
115
|
+
</button>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## useActionState - Form State Management
|
|
121
|
+
|
|
122
|
+
Replaces useFormState — manages form submission state with Server Actions.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
'use client';
|
|
126
|
+
|
|
127
|
+
import { useActionState } from 'react';
|
|
128
|
+
|
|
129
|
+
type FormState = {
|
|
130
|
+
success?: boolean;
|
|
131
|
+
errors?: Record<string, string[]>;
|
|
132
|
+
message?: string;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
type FormData = {
|
|
136
|
+
email: string;
|
|
137
|
+
password: string;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Server Action
|
|
141
|
+
async function login(
|
|
142
|
+
prevState: FormState,
|
|
143
|
+
formData: FormData
|
|
144
|
+
): Promise<FormState> {
|
|
145
|
+
'use server';
|
|
146
|
+
|
|
147
|
+
const email = formData.get('email');
|
|
148
|
+
const password = formData.get('password');
|
|
149
|
+
|
|
150
|
+
if (!email || typeof email !== 'string') {
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
errors: { email: ['Email is required'] },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!password || typeof password !== 'string') {
|
|
158
|
+
return {
|
|
159
|
+
success: false,
|
|
160
|
+
errors: { password: ['Password is required'] },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await signIn(email, password);
|
|
166
|
+
return { success: true, message: 'Logged in successfully' };
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
message: 'Invalid credentials',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Client Component
|
|
176
|
+
export function LoginForm() {
|
|
177
|
+
const [state, formAction, isPending] = useActionState<FormState, FormData>(
|
|
178
|
+
login,
|
|
179
|
+
{} // Initial state
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<form action={formAction}>
|
|
184
|
+
<div>
|
|
185
|
+
<label htmlFor="email">Email</label>
|
|
186
|
+
<input
|
|
187
|
+
id="email"
|
|
188
|
+
name="email"
|
|
189
|
+
type="email"
|
|
190
|
+
required
|
|
191
|
+
aria-invalid={!!state.errors?.email}
|
|
192
|
+
/>
|
|
193
|
+
{state.errors?.email?.map((error) => (
|
|
194
|
+
<p key={error} className="error">{error}</p>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div>
|
|
199
|
+
<label htmlFor="password">Password</label>
|
|
200
|
+
<input
|
|
201
|
+
id="password"
|
|
202
|
+
name="password"
|
|
203
|
+
type="password"
|
|
204
|
+
required
|
|
205
|
+
aria-invalid={!!state.errors?.password}
|
|
206
|
+
/>
|
|
207
|
+
{state.errors?.password?.map((error) => (
|
|
208
|
+
<p key={error} className="error">{error}</p>
|
|
209
|
+
))}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{state.message && (
|
|
213
|
+
<div className={state.success ? 'success' : 'error'}>
|
|
214
|
+
{state.message}
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
<button type="submit" disabled={isPending}>
|
|
219
|
+
{isPending ? 'Logging in...' : 'Log In'}
|
|
220
|
+
</button>
|
|
221
|
+
</form>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### useActionState with Optimistic Updates
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
'use client';
|
|
230
|
+
|
|
231
|
+
import { useActionState, useOptimistic } from 'react';
|
|
232
|
+
|
|
233
|
+
type Todo = { id: string; title: string; completed: boolean };
|
|
234
|
+
|
|
235
|
+
async function toggleTodo(
|
|
236
|
+
prevState: { todos: Todo[] },
|
|
237
|
+
formData: FormData
|
|
238
|
+
): Promise<{ todos: Todo[] }> {
|
|
239
|
+
'use server';
|
|
240
|
+
|
|
241
|
+
const todoId = formData.get('todoId') as string;
|
|
242
|
+
await db.todo.update({
|
|
243
|
+
where: { id: todoId },
|
|
244
|
+
data: { completed: { toggle: true } },
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const todos = await db.todo.findMany();
|
|
248
|
+
return { todos };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
|
|
252
|
+
const [state, formAction] = useActionState(
|
|
253
|
+
toggleTodo,
|
|
254
|
+
{ todos: initialTodos }
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
|
|
258
|
+
state.todos,
|
|
259
|
+
(currentTodos, todoId: string) =>
|
|
260
|
+
currentTodos.map((todo) =>
|
|
261
|
+
todo.id === todoId ? { ...todo, completed: !todo.completed } : todo
|
|
262
|
+
)
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<ul>
|
|
267
|
+
{optimisticTodos.map((todo) => (
|
|
268
|
+
<li key={todo.id}>
|
|
269
|
+
<form
|
|
270
|
+
action={(formData) => {
|
|
271
|
+
setOptimisticTodos(todo.id);
|
|
272
|
+
formAction(formData);
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
<input type="hidden" name="todoId" value={todo.id} />
|
|
276
|
+
<button type="submit">
|
|
277
|
+
{todo.completed ? '✓' : '○'} {todo.title}
|
|
278
|
+
</button>
|
|
279
|
+
</form>
|
|
280
|
+
</li>
|
|
281
|
+
))}
|
|
282
|
+
</ul>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## use() Hook - Unwrapping Resources
|
|
288
|
+
|
|
289
|
+
use() unwraps promises and context — enables new patterns for data fetching.
|
|
290
|
+
|
|
291
|
+
### use() with Promises
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// Server Component
|
|
295
|
+
async function UserPage({ params }: { params: { id: string } }) {
|
|
296
|
+
// Pass promise without awaiting
|
|
297
|
+
const userPromise = fetchUser(params.id);
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<Suspense fallback={<UserSkeleton />}>
|
|
301
|
+
<UserProfile userPromise={userPromise} />
|
|
302
|
+
</Suspense>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Client Component
|
|
307
|
+
'use client';
|
|
308
|
+
|
|
309
|
+
import { use } from 'react';
|
|
310
|
+
|
|
311
|
+
type UserProfileProps = {
|
|
312
|
+
userPromise: Promise<User>;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
export function UserProfile({ userPromise }: UserProfileProps) {
|
|
316
|
+
// Suspends until resolved
|
|
317
|
+
const user = use(userPromise);
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<div>
|
|
321
|
+
<h1>{user.name}</h1>
|
|
322
|
+
<p>{user.email}</p>
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Conditional use()
|
|
329
|
+
|
|
330
|
+
use() can be called conditionally — unlike hooks.
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
'use client';
|
|
334
|
+
|
|
335
|
+
import { use } from 'react';
|
|
336
|
+
|
|
337
|
+
type Props = {
|
|
338
|
+
userPromise?: Promise<User>;
|
|
339
|
+
userId?: string;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export function UserDisplay({ userPromise, userId }: Props) {
|
|
343
|
+
let user: User | undefined;
|
|
344
|
+
|
|
345
|
+
if (userPromise) {
|
|
346
|
+
user = use(userPromise); // Conditional use() - allowed!
|
|
347
|
+
} else if (userId) {
|
|
348
|
+
// Fetch inline
|
|
349
|
+
user = use(fetchUser(userId));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!user) return <div>No user data</div>;
|
|
353
|
+
|
|
354
|
+
return <div>{user.name}</div>;
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### use() in Loops
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
'use client';
|
|
362
|
+
|
|
363
|
+
import { use } from 'react';
|
|
364
|
+
|
|
365
|
+
type Props = {
|
|
366
|
+
userPromises: Promise<User>[];
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export function UserList({ userPromises }: Props) {
|
|
370
|
+
return (
|
|
371
|
+
<ul>
|
|
372
|
+
{userPromises.map((promise, index) => {
|
|
373
|
+
const user = use(promise); // use() in loop - allowed!
|
|
374
|
+
return <li key={user.id}>{user.name}</li>;
|
|
375
|
+
})}
|
|
376
|
+
</ul>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### use() with Context
|
|
382
|
+
|
|
383
|
+
Alternative to useContext — can be called conditionally.
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
import { createContext, use } from 'react';
|
|
387
|
+
|
|
388
|
+
type Theme = 'light' | 'dark';
|
|
389
|
+
const ThemeContext = createContext<Theme>('light');
|
|
390
|
+
|
|
391
|
+
export function ThemedComponent({ override }: { override?: Theme }) {
|
|
392
|
+
let theme: Theme;
|
|
393
|
+
|
|
394
|
+
if (override) {
|
|
395
|
+
theme = override;
|
|
396
|
+
} else {
|
|
397
|
+
theme = use(ThemeContext); // Conditional context access
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return <div className={theme}>Content</div>;
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## useOptimistic - Optimistic UI Updates
|
|
405
|
+
|
|
406
|
+
Show immediate UI feedback before server confirms.
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
'use client';
|
|
410
|
+
|
|
411
|
+
import { useOptimistic } from 'react';
|
|
412
|
+
|
|
413
|
+
type Message = { id: string; text: string; sending?: boolean };
|
|
414
|
+
|
|
415
|
+
export function MessageThread({ messages }: { messages: Message[] }) {
|
|
416
|
+
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
|
|
417
|
+
messages,
|
|
418
|
+
(state, newMessage: Message) => [...state, newMessage]
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
async function sendMessage(formData: FormData) {
|
|
422
|
+
const text = formData.get('message') as string;
|
|
423
|
+
|
|
424
|
+
// Add optimistic message immediately
|
|
425
|
+
addOptimisticMessage({ id: 'temp', text, sending: true });
|
|
426
|
+
|
|
427
|
+
// Send to server
|
|
428
|
+
await fetch('/api/messages', {
|
|
429
|
+
method: 'POST',
|
|
430
|
+
body: JSON.stringify({ text }),
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<div>
|
|
436
|
+
<ul>
|
|
437
|
+
{optimisticMessages.map((msg) => (
|
|
438
|
+
<li key={msg.id} className={msg.sending ? 'opacity-50' : ''}>
|
|
439
|
+
{msg.text}
|
|
440
|
+
</li>
|
|
441
|
+
))}
|
|
442
|
+
</ul>
|
|
443
|
+
|
|
444
|
+
<form action={sendMessage}>
|
|
445
|
+
<input name="message" required />
|
|
446
|
+
<button type="submit">Send</button>
|
|
447
|
+
</form>
|
|
448
|
+
</div>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### useOptimistic with State Updates
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
'use client';
|
|
457
|
+
|
|
458
|
+
import { useOptimistic, useState, useTransition } from 'react';
|
|
459
|
+
|
|
460
|
+
type Item = { id: string; name: string; quantity: number };
|
|
461
|
+
|
|
462
|
+
export function ShoppingCart({ items: initialItems }: { items: Item[] }) {
|
|
463
|
+
const [items, setItems] = useState(initialItems);
|
|
464
|
+
const [isPending, startTransition] = useTransition();
|
|
465
|
+
|
|
466
|
+
const [optimisticItems, updateOptimistic] = useOptimistic(
|
|
467
|
+
items,
|
|
468
|
+
(state, { id, quantity }: { id: string; quantity: number }) =>
|
|
469
|
+
state.map((item) =>
|
|
470
|
+
item.id === id ? { ...item, quantity } : item
|
|
471
|
+
)
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
async function updateQuantity(id: string, quantity: number) {
|
|
475
|
+
// Optimistic update
|
|
476
|
+
updateOptimistic({ id, quantity });
|
|
477
|
+
|
|
478
|
+
// Server update
|
|
479
|
+
startTransition(async () => {
|
|
480
|
+
const updated = await fetch(`/api/cart/${id}`, {
|
|
481
|
+
method: 'PATCH',
|
|
482
|
+
body: JSON.stringify({ quantity }),
|
|
483
|
+
}).then((r) => r.json());
|
|
484
|
+
|
|
485
|
+
setItems(updated);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
<ul>
|
|
491
|
+
{optimisticItems.map((item) => (
|
|
492
|
+
<li key={item.id}>
|
|
493
|
+
{item.name}
|
|
494
|
+
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>
|
|
495
|
+
-
|
|
496
|
+
</button>
|
|
497
|
+
<span>{item.quantity}</span>
|
|
498
|
+
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>
|
|
499
|
+
+
|
|
500
|
+
</button>
|
|
501
|
+
</li>
|
|
502
|
+
))}
|
|
503
|
+
</ul>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## useTransition - Non-Blocking Updates
|
|
509
|
+
|
|
510
|
+
Mark state updates as non-urgent — UI stays responsive.
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
'use client';
|
|
514
|
+
|
|
515
|
+
import { useTransition, useState } from 'react';
|
|
516
|
+
|
|
517
|
+
export function SearchResults() {
|
|
518
|
+
const [query, setQuery] = useState('');
|
|
519
|
+
const [results, setResults] = useState<string[]>([]);
|
|
520
|
+
const [isPending, startTransition] = useTransition();
|
|
521
|
+
|
|
522
|
+
function handleSearch(value: string) {
|
|
523
|
+
setQuery(value); // Urgent - update input immediately
|
|
524
|
+
|
|
525
|
+
startTransition(() => {
|
|
526
|
+
// Non-urgent - can be interrupted
|
|
527
|
+
const filtered = hugeDataset.filter((item) =>
|
|
528
|
+
item.toLowerCase().includes(value.toLowerCase())
|
|
529
|
+
);
|
|
530
|
+
setResults(filtered);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return (
|
|
535
|
+
<div>
|
|
536
|
+
<input
|
|
537
|
+
type="text"
|
|
538
|
+
value={query}
|
|
539
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
540
|
+
placeholder="Search..."
|
|
541
|
+
/>
|
|
542
|
+
|
|
543
|
+
{isPending && <div>Searching...</div>}
|
|
544
|
+
|
|
545
|
+
<ul>
|
|
546
|
+
{results.map((result) => (
|
|
547
|
+
<li key={result}>{result}</li>
|
|
548
|
+
))}
|
|
549
|
+
</ul>
|
|
550
|
+
</div>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### useTransition with Server Actions
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
'use client';
|
|
559
|
+
|
|
560
|
+
import { useTransition } from 'react';
|
|
561
|
+
import { deletePost } from '@/actions/posts';
|
|
562
|
+
|
|
563
|
+
export function DeleteButton({ postId }: { postId: string }) {
|
|
564
|
+
const [isPending, startTransition] = useTransition();
|
|
565
|
+
|
|
566
|
+
function handleDelete() {
|
|
567
|
+
startTransition(async () => {
|
|
568
|
+
await deletePost(postId);
|
|
569
|
+
// UI stays responsive during deletion
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return (
|
|
574
|
+
<button onClick={handleDelete} disabled={isPending}>
|
|
575
|
+
{isPending ? 'Deleting...' : 'Delete'}
|
|
576
|
+
</button>
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
## useDeferredValue - Deferred Rendering
|
|
582
|
+
|
|
583
|
+
Defer expensive re-renders while keeping UI responsive.
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
'use client';
|
|
587
|
+
|
|
588
|
+
import { useDeferredValue, useState } from 'react';
|
|
589
|
+
|
|
590
|
+
export function ProductSearch() {
|
|
591
|
+
const [query, setQuery] = useState('');
|
|
592
|
+
const deferredQuery = useDeferredValue(query);
|
|
593
|
+
|
|
594
|
+
return (
|
|
595
|
+
<div>
|
|
596
|
+
<input
|
|
597
|
+
type="text"
|
|
598
|
+
value={query}
|
|
599
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
600
|
+
placeholder="Search products..."
|
|
601
|
+
/>
|
|
602
|
+
|
|
603
|
+
{/* Uses deferred value - won't block input */}
|
|
604
|
+
<ExpensiveResults query={deferredQuery} />
|
|
605
|
+
</div>
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function ExpensiveResults({ query }: { query: string }) {
|
|
610
|
+
const results = useMemo(() => {
|
|
611
|
+
// Expensive filtering/sorting
|
|
612
|
+
return products.filter((p) => p.name.includes(query));
|
|
613
|
+
}, [query]);
|
|
614
|
+
|
|
615
|
+
return (
|
|
616
|
+
<ul>
|
|
617
|
+
{results.map((result) => (
|
|
618
|
+
<li key={result.id}>{result.name}</li>
|
|
619
|
+
))}
|
|
620
|
+
</ul>
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
## Migration Checklist
|
|
626
|
+
|
|
627
|
+
Updating from React 18 to React 19:
|
|
628
|
+
|
|
629
|
+
- [ ] Replace forwardRef with ref as prop
|
|
630
|
+
- [ ] Replace useFormState with useActionState
|
|
631
|
+
- [ ] Update Server Action types to include prevState parameter
|
|
632
|
+
- [ ] Use use() for promises in Server Components
|
|
633
|
+
- [ ] Add 'use server' directive to Server Actions
|
|
634
|
+
- [ ] Add 'use client' directive to Client Components
|
|
635
|
+
- [ ] Update TypeScript to 5.0+ for better React 19 support
|
|
636
|
+
- [ ] Update @types/react to 19.x
|
|
637
|
+
- [ ] Test all forms with useActionState
|
|
638
|
+
- [ ] Verify ref forwarding works without forwardRef
|