Initial commit
This commit is contained in:
213
skills/composing-components/SKILL.md
Normal file
213
skills/composing-components/SKILL.md
Normal file
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: composing-components
|
||||
description: Teaches component composition patterns in React 19 including children prop, compound components, and render props. Use when designing component APIs, creating reusable components, or avoiding prop drilling.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Component Composition Patterns
|
||||
|
||||
<role>
|
||||
This skill teaches you how to compose components effectively using React 19 patterns.
|
||||
</role>
|
||||
|
||||
<when-to-activate>
|
||||
This skill activates when:
|
||||
|
||||
- Designing reusable component APIs
|
||||
- Need to avoid prop drilling
|
||||
- Creating compound components (Tabs, Accordion, etc.)
|
||||
- Building flexible, composable interfaces
|
||||
- Choosing between composition patterns
|
||||
</when-to-activate>
|
||||
|
||||
<overview>
|
||||
React 19 supports multiple composition patterns:
|
||||
|
||||
1. **Children Prop** - Simplest composition, pass components as children
|
||||
2. **Compound Components** - Components that work together (Context-based)
|
||||
3. **Render Props** - Functions as children for flexibility
|
||||
4. **Composition over Props** - Prefer slots over configuration props
|
||||
|
||||
**When to Use:**
|
||||
|
||||
- Children prop: Simple containment
|
||||
- Compound components: Coordinated behavior (tabs, accordions)
|
||||
- Render props: Custom rendering logic
|
||||
- Slots: Multiple insertion points
|
||||
</overview>
|
||||
|
||||
<workflow>
|
||||
## Pattern 1: Children Prop
|
||||
|
||||
```javascript
|
||||
function Card({ children, header, footer }) {
|
||||
return (
|
||||
<div className="card">
|
||||
{header && <div className="header">{header}</div>}
|
||||
<div className="body">{children}</div>
|
||||
{footer && <div className="footer">{footer}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<Card header={<h2>Title</h2>} footer={<button>Action</button>}>
|
||||
<p>Card content goes here</p>
|
||||
</Card>;
|
||||
```
|
||||
|
||||
## Pattern 2: Compound Components
|
||||
|
||||
```javascript
|
||||
import { createContext, use } from 'react';
|
||||
|
||||
const TabsContext = createContext(null);
|
||||
|
||||
export function Tabs({ children, defaultTab }) {
|
||||
const [activeTab, setActiveTab] = useState(defaultTab);
|
||||
|
||||
return (
|
||||
<TabsContext value={{ activeTab, setActiveTab }}>
|
||||
<div className="tabs">{children}</div>
|
||||
</TabsContext>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabList({ children }) {
|
||||
return <div className="tab-list">{children}</div>;
|
||||
}
|
||||
|
||||
export function Tab({ id, children }) {
|
||||
const { activeTab, setActiveTab } = use(TabsContext);
|
||||
|
||||
return (
|
||||
<button className={activeTab === id ? 'active' : ''} onClick={() => setActiveTab(id)}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabPanel({ id, children }) {
|
||||
const { activeTab } = use(TabsContext);
|
||||
|
||||
if (activeTab !== id) return null;
|
||||
|
||||
return <div className="tab-panel">{children}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```javascript
|
||||
<Tabs defaultTab="profile">
|
||||
<TabList>
|
||||
<Tab id="profile">Profile</Tab>
|
||||
<Tab id="settings">Settings</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel id="profile">
|
||||
<Profile />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel id="settings">
|
||||
<Settings />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
## Pattern 3: Render Props
|
||||
|
||||
```javascript
|
||||
function DataProvider({ render, endpoint }) {
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(endpoint)
|
||||
.then((res) => res.json())
|
||||
.then(setData);
|
||||
}, [endpoint]);
|
||||
|
||||
return render({ data, loading: !data });
|
||||
}
|
||||
|
||||
<DataProvider
|
||||
endpoint="/api/users"
|
||||
render={({ data, loading }) => (loading ? <Spinner /> : <UserList users={data} />)}
|
||||
/>;
|
||||
```
|
||||
|
||||
</workflow>
|
||||
|
||||
<examples>
|
||||
## Example: Modal with Composition
|
||||
|
||||
```javascript
|
||||
function Modal({ children, isOpen, onClose }) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ModalHeader({ children }) {
|
||||
return <div className="modal-header">{children}</div>;
|
||||
}
|
||||
|
||||
function ModalBody({ children }) {
|
||||
return <div className="modal-body">{children}</div>;
|
||||
}
|
||||
|
||||
function ModalFooter({ children }) {
|
||||
return <div className="modal-footer">{children}</div>;
|
||||
}
|
||||
|
||||
Modal.Header = ModalHeader;
|
||||
Modal.Body = ModalBody;
|
||||
Modal.Footer = ModalFooter;
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```javascript
|
||||
<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
|
||||
<Modal.Header>
|
||||
<h2>Confirm Action</h2>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body>
|
||||
<p>Are you sure you want to proceed?</p>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<button onClick={() => setShowModal(false)}>Cancel</button>
|
||||
<button onClick={handleConfirm}>Confirm</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
```
|
||||
|
||||
For comprehensive composition patterns, see: `research/react-19-comprehensive.md` lines 1263-1293.
|
||||
</examples>
|
||||
|
||||
<constraints>
|
||||
## MUST
|
||||
- Use `use()` API for Context in React 19 (can be conditional)
|
||||
- Keep compound components cohesive
|
||||
- Document required child components
|
||||
|
||||
## SHOULD
|
||||
|
||||
- Prefer composition over complex prop APIs
|
||||
- Use Context for compound component state
|
||||
- Provide good component names for debugging
|
||||
|
||||
## NEVER
|
||||
|
||||
- Over-engineer simple components
|
||||
- Use Context for non-coordinated components
|
||||
- Forget to handle edge cases (missing children, etc.)
|
||||
</constraints>
|
||||
189
skills/following-the-rules-of-hooks/SKILL.md
Normal file
189
skills/following-the-rules-of-hooks/SKILL.md
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
name: following-the-rules-of-hooks
|
||||
description: Fix React Rules of Hooks violations - conditional calls, hooks in loops/callbacks/classes
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Rules of Hooks
|
||||
|
||||
React enforces two invariants on Hook usage. Violating these causes state corruption and unpredictable behavior.
|
||||
|
||||
## The Rules
|
||||
|
||||
1. **Top-level only** - Never call Hooks inside loops, conditions, nested functions, or try/catch/finally
|
||||
2. **React functions only** - Call Hooks exclusively from function components or custom Hooks
|
||||
|
||||
**Why:** Consistent call order across renders; conditional/dynamic invocation breaks state tracking.
|
||||
|
||||
## Valid Hook Locations
|
||||
|
||||
✅ Top level of function components
|
||||
✅ Top level of custom Hooks (`use*` functions)
|
||||
|
||||
```javascript
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
function useWindowWidth() {
|
||||
const [width, setWidth] = useState(window.innerWidth);
|
||||
return width;
|
||||
}
|
||||
```
|
||||
|
||||
## Common Violations
|
||||
|
||||
| Violation | Why Invalid | Fix |
|
||||
|-----------|-------------|-----|
|
||||
| Inside if/else | Skipped on some renders | Move to top; use conditional rendering |
|
||||
| Inside loops | Variable call count | Move to top; manage array state |
|
||||
| After early return | Unreachable on some paths | Move Hook before return |
|
||||
| In event handlers | Called outside render | Move to top; use state from closure |
|
||||
| In class components | Classes don't support Hooks | Convert to function component |
|
||||
| Inside callbacks | Nested function context | Move Hook to top level |
|
||||
|
||||
## Common Fixes
|
||||
|
||||
### Conditional Hooks
|
||||
❌ **Wrong:**
|
||||
```javascript
|
||||
function Profile({ userId }) {
|
||||
if (userId) {
|
||||
const user = useUser(userId);
|
||||
}
|
||||
}
|
||||
```
|
||||
✅ **Right:**
|
||||
```javascript
|
||||
function Profile({ userId }) {
|
||||
const user = useUser(userId);
|
||||
if (!userId) return null;
|
||||
return <div>{user.name}</div>;
|
||||
}
|
||||
```
|
||||
**Pattern:** Always call Hook, use conditional rendering for output.
|
||||
### Hooks in Loops
|
||||
❌ **Wrong:**
|
||||
```javascript
|
||||
function List({ items }) {
|
||||
return items.map(item => {
|
||||
const [selected, setSelected] = useState(false);
|
||||
return <Item selected={selected} />;
|
||||
});
|
||||
}
|
||||
```
|
||||
✅ **Right:**
|
||||
```javascript
|
||||
function List({ items }) {
|
||||
const [selected, setSelected] = useState({});
|
||||
return items.map(item => (
|
||||
<Item
|
||||
key={item.id}
|
||||
selected={selected[item.id]}
|
||||
onToggle={() => setSelected(s => ({...s, [item.id]: !s[item.id]}))}
|
||||
/>
|
||||
));
|
||||
}
|
||||
```
|
||||
**Pattern:** Single Hook managing collection, not per-item Hooks.
|
||||
### Hooks in Event Handlers
|
||||
❌ **Wrong:**
|
||||
```javascript
|
||||
function Form() {
|
||||
function handleSubmit() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
setLoading(true);
|
||||
}
|
||||
return <button onClick={handleSubmit}>Submit</button>;
|
||||
}
|
||||
```
|
||||
✅ **Right:**
|
||||
```javascript
|
||||
function Form() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
function handleSubmit() {
|
||||
setLoading(true);
|
||||
}
|
||||
return <button onClick={handleSubmit} disabled={loading}>Submit</button>;
|
||||
}
|
||||
```
|
||||
**Pattern:** Hook at component level, setter in handler.
|
||||
### Hooks in Classes
|
||||
❌ **Wrong:**
|
||||
```javascript
|
||||
function BadCounter() {
|
||||
const [count, setCount] = useState(0);
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
```
|
||||
✅ **Right:**
|
||||
```javascript
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
```
|
||||
**Pattern:** Use function components for Hooks.
|
||||
### Hooks in Callbacks
|
||||
❌ **Wrong:**
|
||||
```javascript
|
||||
function Theme() {
|
||||
const style = useMemo(() => {
|
||||
const theme = useContext(ThemeContext);
|
||||
return createStyle(theme);
|
||||
}, []);
|
||||
}
|
||||
```
|
||||
✅ **Right:**
|
||||
```javascript
|
||||
function Theme() {
|
||||
const theme = useContext(ThemeContext);
|
||||
const style = useMemo(() => createStyle(theme), [theme]);
|
||||
}
|
||||
```
|
||||
**Pattern:** Call Hook at top level, reference in callback.
|
||||
### Hooks After Early Returns
|
||||
❌ **Wrong:**
|
||||
```javascript
|
||||
function User({ userId }) {
|
||||
if (!userId) return null;
|
||||
const user = useUser(userId);
|
||||
return <div>{user.name}</div>;
|
||||
}
|
||||
```
|
||||
✅ **Right:**
|
||||
```javascript
|
||||
function User({ userId }) {
|
||||
const user = useUser(userId || null);
|
||||
if (!userId) return null;
|
||||
return <div>{user.name}</div>;
|
||||
}
|
||||
```
|
||||
**Pattern:** Call all Hooks before any returns.
|
||||
|
||||
## Custom Hooks
|
||||
|
||||
Custom Hooks may call other Hooks because they execute during render phase:
|
||||
```javascript
|
||||
function useDebounce(value, delay) {
|
||||
const [debounced, setDebounced] = useState(value);
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebounced(value), delay);
|
||||
return () => clearTimeout(timer);
|
||||
}, [value, delay]);
|
||||
return debounced;
|
||||
}
|
||||
```
|
||||
|
||||
**Requirements:** Name starts with `use`; called from function component or another custom Hook; follows same Rules of Hooks.
|
||||
|
||||
## Quick Diagnostic
|
||||
|
||||
**ESLint error:** "React Hook cannot be called..."
|
||||
1. Check location: Is Hook inside if/loop/try/handler/class?
|
||||
2. Move Hook to top level of component/custom Hook
|
||||
3. Keep conditional logic, move Hook call outside it
|
||||
4. Use conditional rendering, not conditional Hooks
|
||||
|
||||
**Reference:** https://react.dev/reference/rules/rules-of-hooks
|
||||
85
skills/implementing-code-splitting/SKILL.md
Normal file
85
skills/implementing-code-splitting/SKILL.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: implementing-code-splitting
|
||||
description: Teaches code splitting with lazy() and Suspense in React 19 for reducing initial bundle size. Use when implementing lazy loading, route-based splitting, or optimizing performance.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Code Splitting with lazy() and Suspense
|
||||
|
||||
## Basic Pattern
|
||||
|
||||
```javascript
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
const HeavyComponent = lazy(() => import('./HeavyComponent'));
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<HeavyComponent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Route-Based Splitting
|
||||
|
||||
```javascript
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
||||
const Settings = lazy(() => import('./pages/Settings'));
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Component-Based Splitting
|
||||
|
||||
```javascript
|
||||
import { lazy, Suspense, useState } from 'react';
|
||||
|
||||
const Chart = lazy(() => import('./Chart'));
|
||||
const Table = lazy(() => import('./Table'));
|
||||
|
||||
function DataView() {
|
||||
const [view, setView] = useState('chart');
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setView('chart')}>Chart</button>
|
||||
<button onClick={() => setView('table')}>Table</button>
|
||||
|
||||
<Suspense fallback={<Spinner />}>
|
||||
{view === 'chart' ? <Chart /> : <Table />}
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Named Exports
|
||||
|
||||
```javascript
|
||||
const { BarChart } = await import('./Charts');
|
||||
|
||||
export const BarChart = lazy(() =>
|
||||
import('./Charts').then(module => ({ default: module.BarChart }))
|
||||
);
|
||||
```
|
||||
|
||||
For comprehensive code splitting patterns, see: `research/react-19-comprehensive.md` lines 1224-1238.
|
||||
107
skills/implementing-optimistic-updates/SKILL.md
Normal file
107
skills/implementing-optimistic-updates/SKILL.md
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
name: implementing-optimistic-updates
|
||||
description: Teaches useOptimistic hook for immediate UI updates during async operations in React 19. Use when implementing optimistic UI patterns, instant feedback, or reducing perceived latency.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Optimistic UI Updates with useOptimistic
|
||||
|
||||
<role>
|
||||
This skill teaches you how to use React 19's `useOptimistic` hook for immediate UI feedback during async operations.
|
||||
</role>
|
||||
|
||||
<when-to-activate>
|
||||
- User mentions optimistic updates, instant feedback, or perceived performance
|
||||
- Working with mutations that should feel instant (likes, comments, todos)
|
||||
- Need to show pending states before server confirmation
|
||||
</when-to-activate>
|
||||
|
||||
<overview>
|
||||
`useOptimistic` enables immediate UI updates that revert if the operation fails:
|
||||
|
||||
1. Shows anticipated result immediately
|
||||
2. Reverts to actual state when async completes
|
||||
3. Provides better UX than waiting for server
|
||||
4. Works with `startTransition` for async operations
|
||||
</overview>
|
||||
|
||||
<workflow>
|
||||
## Basic Pattern
|
||||
|
||||
```javascript
|
||||
import { useOptimistic, startTransition } from 'react';
|
||||
|
||||
function MessageList({ messages, sendMessage }) {
|
||||
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
|
||||
messages,
|
||||
(state, newMessage) => [...state, { ...newMessage, sending: true }]
|
||||
);
|
||||
|
||||
const handleSend = async (text) => {
|
||||
addOptimisticMessage({ id: Date.now(), text });
|
||||
|
||||
startTransition(async () => {
|
||||
await sendMessage(text);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{optimisticMessages.map((msg) => (
|
||||
<li key={msg.id}>
|
||||
{msg.text} {msg.sending && <small>(Sending...)</small>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
</workflow>
|
||||
|
||||
<examples>
|
||||
## Like Button Example
|
||||
|
||||
```javascript
|
||||
function LikeButton({ postId, initialLikes }) {
|
||||
const [optimisticLikes, addOptimisticLike] = useOptimistic(
|
||||
initialLikes,
|
||||
(state, amount) => state + amount
|
||||
);
|
||||
|
||||
const handleLike = async () => {
|
||||
addOptimisticLike(1);
|
||||
|
||||
startTransition(async () => {
|
||||
await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<button onClick={handleLike}>
|
||||
❤️ {optimisticLikes}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
For comprehensive useOptimistic documentation, see: `research/react-19-comprehensive.md` lines 182-240.
|
||||
</examples>
|
||||
|
||||
<constraints>
|
||||
## MUST
|
||||
- Keep update function pure (no side effects)
|
||||
- Pair with `startTransition` for async operations
|
||||
- Provide visual feedback for pending states
|
||||
|
||||
## NEVER
|
||||
- Mutate state directly in update function
|
||||
- Use for critical operations that must succeed
|
||||
- Skip error handling for failed optimistic updates
|
||||
</constraints>
|
||||
|
||||
<related-skills>
|
||||
## Related Skills
|
||||
|
||||
If handling Prisma transaction errors in optimistic updates, use the handling-transaction-errors skill from prisma-6 for graceful P-code error handling.
|
||||
</related-skills>
|
||||
130
skills/implementing-server-actions/SKILL.md
Normal file
130
skills/implementing-server-actions/SKILL.md
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
name: implementing-server-actions
|
||||
description: Teaches Server Actions in React 19 for form handling and data mutations. Use when implementing forms, mutations, or server-side logic. Server Actions are async functions marked with 'use server'.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Server Actions
|
||||
|
||||
Server Actions are async functions executed on the server, callable from Client Components.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
**Defining Server Actions:**
|
||||
|
||||
```javascript
|
||||
'use server';
|
||||
|
||||
export async function createUser(formData) {
|
||||
const name = formData.get('name');
|
||||
const email = formData.get('email');
|
||||
|
||||
const user = await db.users.create({ name, email });
|
||||
return { success: true, userId: user.id };
|
||||
}
|
||||
```
|
||||
|
||||
**Using in Forms:**
|
||||
|
||||
```javascript
|
||||
'use client';
|
||||
|
||||
import { createUser } from './actions';
|
||||
|
||||
function SignupForm() {
|
||||
return (
|
||||
<form action={createUser}>
|
||||
<input name="name" required />
|
||||
<input name="email" type="email" required />
|
||||
<button type="submit">Sign Up</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Security Requirements
|
||||
|
||||
**MUST validate all inputs:**
|
||||
|
||||
```javascript
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export async function createUser(formData) {
|
||||
const result = schema.safeParse({
|
||||
name: formData.get('name'),
|
||||
email: formData.get('email'),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return { error: result.error.flatten().fieldErrors };
|
||||
}
|
||||
|
||||
const user = await db.users.create(result.data);
|
||||
return { success: true, userId: user.id };
|
||||
}
|
||||
```
|
||||
|
||||
**MUST check authentication:**
|
||||
|
||||
```javascript
|
||||
'use server';
|
||||
|
||||
export async function deletePost(postId) {
|
||||
const session = await getSession();
|
||||
|
||||
if (!session?.user) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
const post = await db.posts.findUnique({ where: { id: postId } });
|
||||
|
||||
if (post.authorId !== session.user.id) {
|
||||
throw new Error('Forbidden');
|
||||
}
|
||||
|
||||
await db.posts.delete({ where: { id: postId } });
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
## Progressive Enhancement
|
||||
|
||||
Server Actions work before JavaScript loads:
|
||||
|
||||
```javascript
|
||||
'use client';
|
||||
|
||||
import { useActionState } from 'react';
|
||||
import { submitForm } from './actions';
|
||||
|
||||
function Form() {
|
||||
const [state, formAction] = useActionState(
|
||||
submitForm,
|
||||
null,
|
||||
'/api/submit'
|
||||
);
|
||||
|
||||
return <form action={formAction}>...</form>;
|
||||
}
|
||||
```
|
||||
|
||||
For comprehensive Server Actions documentation, see: `research/react-19-comprehensive.md` lines 644-733.
|
||||
|
||||
## Related Skills
|
||||
|
||||
**Zod v4 Error Handling:**
|
||||
- handling-zod-errors skill from the zod-4 plugin - SafeParse pattern, error flattening, and user-friendly error messages for server action validation
|
||||
|
||||
**Prisma 6 Integration:**
|
||||
- If setting up PrismaClient singleton pattern for Server Actions in serverless Next.js, use the creating-client-singletons skill from prisma-6 for proper initialization preventing connection pool exhaustion.
|
||||
- If validating database inputs in server actions, use the validating-query-inputs skill from prisma-6 for Zod validation pipeline patterns
|
||||
- If ensuring type-safe Prisma queries with GetPayload patterns, use the ensuring-query-type-safety skill from prisma-6 for type-safe database operations
|
||||
- For multi-step mutations and complex database operations, use the handling-transaction-errors skill from prisma-6 for comprehensive transaction error handling in server actions
|
||||
119
skills/managing-local-vs-global-state/SKILL.md
Normal file
119
skills/managing-local-vs-global-state/SKILL.md
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: managing-local-vs-global-state
|
||||
description: Teaches when to use local state vs global state (Context) in React 19. Use when deciding state management strategy, avoiding prop drilling, or architecting component state.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Local vs Global State
|
||||
|
||||
## Decision Flow
|
||||
|
||||
**Use Local State (useState) when:**
|
||||
- State only needed in one component
|
||||
- State only needed by component + direct children
|
||||
- State changes frequently (avoid Context re-renders)
|
||||
- State is UI-specific (form input, toggle, etc.)
|
||||
|
||||
**Use Lifted State when:**
|
||||
- Two sibling components need to share state
|
||||
- Parent coordinates between children
|
||||
- Still contained to component subtree
|
||||
|
||||
**Use Context when:**
|
||||
- Many components at different nesting levels need state
|
||||
- Prop drilling through 3+ levels
|
||||
- Global configuration (theme, locale, auth)
|
||||
- State changes infrequently
|
||||
|
||||
## Examples
|
||||
|
||||
**Local State:**
|
||||
```javascript
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
**Lifted State:**
|
||||
```javascript
|
||||
function Parent() {
|
||||
const [filter, setFilter] = useState('all');
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterButtons filter={filter} setFilter={setFilter} />
|
||||
<ItemList filter={filter} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Context (React 19 with `use()`):**
|
||||
```javascript
|
||||
import { createContext, use } from 'react';
|
||||
|
||||
const ThemeContext = createContext('light');
|
||||
|
||||
function App() {
|
||||
const [theme, setTheme] = useState('light');
|
||||
|
||||
return (
|
||||
<ThemeContext value={{ theme, setTheme }}>
|
||||
<Layout />
|
||||
</ThemeContext>
|
||||
);
|
||||
}
|
||||
|
||||
function DeepComponent() {
|
||||
const { theme, setTheme } = use(ThemeContext);
|
||||
|
||||
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
|
||||
Toggle Theme
|
||||
</button>;
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
❌ **Context for frequently changing state:**
|
||||
```javascript
|
||||
const MousePositionContext = createContext();
|
||||
|
||||
function App() {
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e) => setPosition({ x: e.clientX, y: e.clientY });
|
||||
window.addEventListener('mousemove', handler);
|
||||
return () => window.removeEventListener('mousemove', handler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MousePositionContext value={position}>
|
||||
<App />
|
||||
</MousePositionContext>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This causes re-render of entire tree on every mouse move!
|
||||
|
||||
✅ **Local state or ref instead:**
|
||||
```javascript
|
||||
function Component() {
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e) => setPosition({ x: e.clientX, y: e.clientY });
|
||||
window.addEventListener('mousemove', handler);
|
||||
return () => window.removeEventListener('mousemove', handler);
|
||||
}, []);
|
||||
|
||||
return <div>Mouse: {position.x}, {position.y}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
For comprehensive state management patterns, see: `research/react-19-comprehensive.md` lines 1293-1342.
|
||||
238
skills/managing-server-vs-client-boundaries/SKILL.md
Normal file
238
skills/managing-server-vs-client-boundaries/SKILL.md
Normal file
@@ -0,0 +1,238 @@
|
||||
---
|
||||
name: managing-server-vs-client-boundaries
|
||||
description: Teaches when to use Server Components vs Client Components in React 19, including the 'use client' directive and boundary patterns. Use when architecting components, choosing component types, or working with Server Components.
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Server vs Client Component Boundaries
|
||||
|
||||
**Role**: Choose between Server and Client Components effectively and manage boundaries between them.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- User mentions Server Components, Client Components, or `'use client'`
|
||||
- Architecting component hierarchy
|
||||
- Accessing server-only or client-only APIs
|
||||
- Working with React Server Components frameworks (Next.js, Remix)
|
||||
- Errors about hooks or browser APIs in Server Components
|
||||
|
||||
## Component Comparison
|
||||
|
||||
| Feature | Server Component | Client Component |
|
||||
| ----------------------------------- | ---------------- | --------------------- |
|
||||
| Directive | None (default) | `'use client'` |
|
||||
| Hooks; Event handlers; Browser APIs | ❌ | ✅ |
|
||||
| Async/await (top-level) | ✅ | ⚠️ Limited |
|
||||
| Database/server APIs | ✅ Direct | ❌ Use Server Actions |
|
||||
| Import Server Components | ✅ | ❌ Pass as children |
|
||||
| Bundle impact | 📦 Zero | 📦 Sent to client |
|
||||
| Bundle reduction | 20%-90% | — |
|
||||
|
||||
**Key Decision**: Needs interactivity/hooks/browser APIs → Client Component; static/server-only data → Server Component
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
Choose **Client Component** if: needs hooks, event handlers, browser APIs, or state management.
|
||||
|
||||
Choose **Server Component** if: purely presentational, fetches server data, accesses databases/server APIs.
|
||||
|
||||
## Implementing Components
|
||||
|
||||
**Step 1: Add `'use client'` at file top** (Client Components only)
|
||||
|
||||
```javascript
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Compose Server + Client**
|
||||
|
||||
Server Components import Client Components; pass Server Components as children to avoid circular imports:
|
||||
|
||||
```javascript
|
||||
// Server Component: Can fetch data, import CC
|
||||
import { Counter } from './Counter';
|
||||
|
||||
async function Page() {
|
||||
const data = await db.getData();
|
||||
return (
|
||||
<div>
|
||||
<h1>{data.title}</h1>
|
||||
<Counter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ WRONG: Client Component importing Server Component
|
||||
('use client');
|
||||
import ServerComponent from './ServerComponent'; // ERROR
|
||||
|
||||
// ✅ RIGHT: Pass Server Component as children
|
||||
<ClientWrapper>
|
||||
<ServerComponent />
|
||||
</ClientWrapper>;
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Interactivity at leaf nodes**: Server Component fetches data, passes to Client Component for interaction
|
||||
|
||||
```javascript
|
||||
async function ProductPage({ id }) {
|
||||
const product = await db.products.find(id);
|
||||
return (
|
||||
<>
|
||||
<ProductDetails product={product} />
|
||||
<AddToCartButton productId={id} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Server data in Client Component**: Pass as props (serialize data)
|
||||
|
||||
```javascript
|
||||
async function ServerComponent() {
|
||||
const data = await fetchData();
|
||||
return <ClientComponent data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Server logic from Client**: Use Server Actions
|
||||
|
||||
```javascript
|
||||
async function ServerComponent() {
|
||||
async function serverAction() {
|
||||
'use server';
|
||||
await db.update(...);
|
||||
}
|
||||
return <ClientForm action
|
||||
|
||||
={serverAction} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Product Page with Cart
|
||||
|
||||
```javascript
|
||||
// Server Component
|
||||
import { AddToCart } from './AddToCart';
|
||||
import { Reviews } from './Reviews';
|
||||
|
||||
async function ProductPage({ productId }) {
|
||||
const product = await db.products.find(productId);
|
||||
const reviews = await db.reviews.findByProduct(productId);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<img src={product.image} alt={product.name} />
|
||||
<section>
|
||||
<h1>{product.name}</h1>
|
||||
<p>{product.description}</p>
|
||||
<p>${product.price}</p>
|
||||
<AddToCart productId={productId} />
|
||||
</section>
|
||||
<Reviews reviews={reviews} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// Client Component
|
||||
('use client');
|
||||
import { useState } from 'react';
|
||||
|
||||
export function AddToCart({ productId }) {
|
||||
const [adding, setAdding] = useState(false);
|
||||
|
||||
async function handleAdd() {
|
||||
setAdding(true);
|
||||
await fetch('/api/cart', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ productId }),
|
||||
});
|
||||
setAdding(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleAdd} disabled={adding}>
|
||||
{adding ? 'Adding...' : 'Add to Cart'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Server Component with real-time Client
|
||||
async function Dashboard() {
|
||||
const stats = await db.stats.getLatest();
|
||||
return (
|
||||
<>
|
||||
<DashboardStats stats={stats} />
|
||||
<LiveMetrics />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Client Component with WebSocket
|
||||
('use client');
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function LiveMetrics() {
|
||||
const [metrics, setMetrics] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket('/api/metrics');
|
||||
ws.onmessage = (event) => setMetrics(JSON.parse(event.data));
|
||||
return () => ws.close();
|
||||
}, []);
|
||||
|
||||
return metrics ? <div>Active Users: {metrics.activeUsers}</div> : <div>Connecting...</div>;
|
||||
}
|
||||
|
||||
// Server Component with Server Action
|
||||
async function ContactPage() {
|
||||
async function submitContact(formData) {
|
||||
'use server';
|
||||
await db.contacts.create({
|
||||
email: formData.get('email'),
|
||||
message: formData.get('message'),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={submitContact}>
|
||||
<input name="email" type="email" />
|
||||
<textarea name="message" />
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
**MUST**: `'use client'` at file top for Client Components; before any imports; serialize props (Server → Client); use Server Actions for server-side logic from Client
|
||||
|
||||
**SHOULD**: Keep most components as Server Components (smaller bundle); place `'use client'` at leaf nodes (smallest boundary); use Server Components for data fetching; use Client Components only for interactivity
|
||||
|
||||
**NEVER**: Import Server Components into Client Components; use hooks in Server Components; access browser APIs in Server Components; pass non-serializable props (functions, classes, symbols); om
|
||||
|
||||
it `'use client'` directive
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
1. **Component Types**: Client Components have `'use client'` at top; Server Components have no directive; no hooks/event handlers in Server Components
|
||||
2. **Data Flow**: Server → Client props are serializable; Client → Server uses Server Actions; no Server Components imported in Client
|
||||
3. **Functionality**: Server Components fetch data correctly; Client Components handle interaction; no hydration mismatches; no runtime errors about hooks/browser APIs
|
||||
4. **Bundle**: Only necessary components are Client Components; most stay on server; JavaScript minimized
|
||||
|
||||
## References
|
||||
|
||||
- **Server Components**: `research/react-19-comprehensive.md` (lines 71-82)
|
||||
- **Server Actions**: `forms/skills/server-actions/SKILL.md`
|
||||
- **Component Composition**: `component-composition/SKILL.md`
|
||||
594
skills/migrating-from-forwardref/SKILL.md
Normal file
594
skills/migrating-from-forwardref/SKILL.md
Normal file
@@ -0,0 +1,594 @@
|
||||
---
|
||||
name: migrating-from-forwardref
|
||||
description: Teaches migration from forwardRef to ref-as-prop pattern in React 19. Use when seeing forwardRef usage, upgrading React components, or when refs are mentioned. forwardRef is deprecated in React 19.
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Migrating from forwardRef to Ref as Prop
|
||||
|
||||
<role>
|
||||
This skill teaches you how to migrate from the deprecated `forwardRef` API to React 19's ref-as-prop pattern.
|
||||
</role>
|
||||
|
||||
<when-to-activate>
|
||||
This skill activates when:
|
||||
|
||||
- User mentions `forwardRef`, refs, or ref forwarding
|
||||
- Seeing code that uses `React.forwardRef`
|
||||
- Upgrading components to React 19
|
||||
- Need to expose DOM refs from custom components
|
||||
- TypeScript errors about ref props
|
||||
</when-to-activate>
|
||||
|
||||
<overview>
|
||||
React 19 deprecates `forwardRef` in favor of ref as a regular prop:
|
||||
|
||||
**Why the Change:**
|
||||
1. **Simpler API** - Refs are just props, no special wrapper needed
|
||||
2. **Better TypeScript** - Easier type inference and typing
|
||||
3. **Consistency** - All props handled the same way
|
||||
4. **Less Boilerplate** - Fewer imports and wrapper functions
|
||||
|
||||
**Migration Path:**
|
||||
- `forwardRef` still works in React 19 (deprecated, not removed)
|
||||
- New code should use ref as prop
|
||||
- Gradual migration recommended for existing codebases
|
||||
|
||||
**Key Difference:**
|
||||
```javascript
|
||||
// OLD: forwardRef (deprecated)
|
||||
const Button = forwardRef((props, ref) => ...);
|
||||
|
||||
// NEW: ref as prop (React 19)
|
||||
function Button({ ref, ...props }) { ... }
|
||||
```
|
||||
</overview>
|
||||
|
||||
<workflow>
|
||||
## Migration Process
|
||||
|
||||
**Step 1: Identify forwardRef Usage**
|
||||
|
||||
Search codebase for `forwardRef`:
|
||||
```bash
|
||||
# Use Grep tool
|
||||
pattern: "forwardRef"
|
||||
output_mode: "files_with_matches"
|
||||
```
|
||||
|
||||
**Step 2: Understand Current Pattern**
|
||||
|
||||
Before (React 18):
|
||||
```javascript
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
const MyButton = forwardRef((props, ref) => {
|
||||
return (
|
||||
<button ref={ref} className={props.className}>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
**Step 3: Convert to Ref as Prop**
|
||||
|
||||
After (React 19):
|
||||
```javascript
|
||||
function MyButton({ children, className, ref }) {
|
||||
return (
|
||||
<button ref={ref} className={className}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Update TypeScript Types (if applicable)**
|
||||
|
||||
Before:
|
||||
```typescript
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
interface ButtonProps {
|
||||
variant: 'primary' | 'secondary';
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ variant, children }, ref) => {
|
||||
return (
|
||||
<button ref={ref} className={variant}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
After:
|
||||
```typescript
|
||||
import { Ref } from 'react';
|
||||
|
||||
interface ButtonProps {
|
||||
variant: 'primary' | 'secondary';
|
||||
children: React.ReactNode;
|
||||
ref?: Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
function Button({ variant, children, ref }: ButtonProps) {
|
||||
return (
|
||||
<button ref={ref} className={variant}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: Test Component**
|
||||
|
||||
Verify ref forwarding still works:
|
||||
```javascript
|
||||
function Parent() {
|
||||
const buttonRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
buttonRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
return <Button ref={buttonRef}>Click me</Button>;
|
||||
}
|
||||
```
|
||||
|
||||
</workflow>
|
||||
|
||||
<conditional-workflows>
|
||||
## Complex Scenarios
|
||||
|
||||
**If component uses useImperativeHandle:**
|
||||
|
||||
Before:
|
||||
```javascript
|
||||
const FancyInput = forwardRef((props, ref) => {
|
||||
const inputRef = useRef();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => inputRef.current.focus(),
|
||||
clear: () => { inputRef.current.value = ''; }
|
||||
}));
|
||||
|
||||
return <input ref={inputRef} />;
|
||||
});
|
||||
```
|
||||
|
||||
After:
|
||||
```javascript
|
||||
function FancyInput({ ref }) {
|
||||
const inputRef = useRef();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => inputRef.current.focus(),
|
||||
clear: () => { inputRef.current.value = ''; }
|
||||
}));
|
||||
|
||||
return <input ref={inputRef} />;
|
||||
}
|
||||
```
|
||||
|
||||
**If component has multiple refs:**
|
||||
|
||||
```javascript
|
||||
function ComplexComponent({ ref, innerRef, ...props }) {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<input ref={innerRef} {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**If using generic components:**
|
||||
|
||||
```typescript
|
||||
interface GenericProps<T> {
|
||||
value: T;
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
function GenericComponent<T>({ value, ref }: GenericProps<T>) {
|
||||
return <div ref={ref}>{String(value)}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
</conditional-workflows>
|
||||
|
||||
<progressive-disclosure>
|
||||
## Reference Files
|
||||
|
||||
For detailed information:
|
||||
|
||||
- **Ref Cleanup Functions**: See `../../../research/react-19-comprehensive.md` (lines 1013-1033)
|
||||
- **useImperativeHandle**: See `../../../research/react-19-comprehensive.md` (lines 614-623)
|
||||
- **TypeScript Migration**: See `../../../research/react-19-comprehensive.md` (lines 890-916)
|
||||
- **Complete Migration Guide**: See `../../../research/react-19-comprehensive.md` (lines 978-1011)
|
||||
|
||||
Load references when specific patterns are needed.
|
||||
</progressive-disclosure>
|
||||
|
||||
<examples>
|
||||
## Example 1: Simple Button Migration
|
||||
|
||||
**Before (React 18 with forwardRef):**
|
||||
|
||||
```javascript
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
const Button = forwardRef((props, ref) => (
|
||||
<button ref={ref} {...props}>
|
||||
{props.children}
|
||||
</button>
|
||||
));
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export default Button;
|
||||
```
|
||||
|
||||
**After (React 19 with ref prop):**
|
||||
|
||||
```javascript
|
||||
function Button({ children, ref, ...props }) {
|
||||
return (
|
||||
<button ref={ref} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default Button;
|
||||
```
|
||||
|
||||
**Changes Made:**
|
||||
1. ✅ Removed `forwardRef` import
|
||||
2. ✅ Removed `forwardRef` wrapper
|
||||
3. ✅ Added `ref` to props destructuring
|
||||
4. ✅ Removed unnecessary `displayName`
|
||||
5. ✅ Simplified function signature
|
||||
|
||||
## Example 2: TypeScript Component with Multiple Props
|
||||
|
||||
**Before:**
|
||||
|
||||
```typescript
|
||||
import { forwardRef, HTMLAttributes } from 'react';
|
||||
|
||||
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
title: string;
|
||||
description?: string;
|
||||
variant?: 'default' | 'outlined';
|
||||
}
|
||||
|
||||
const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
({ title, description, variant = 'default', ...props }, ref) => {
|
||||
return (
|
||||
<div ref={ref} className={`card card-${variant}`} {...props}>
|
||||
<h3>{title}</h3>
|
||||
{description && <p>{description}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
export default Card;
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```typescript
|
||||
import { Ref, HTMLAttributes } from 'react';
|
||||
|
||||
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
title: string;
|
||||
description?: string;
|
||||
variant?: 'default' | 'outlined';
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
function Card({
|
||||
title,
|
||||
description,
|
||||
variant = 'default',
|
||||
ref,
|
||||
...props
|
||||
}: CardProps) {
|
||||
return (
|
||||
<div ref={ref} className={`card card-${variant}`} {...props}>
|
||||
<h3>{title}</h3>
|
||||
{description && <p>{description}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Card;
|
||||
```
|
||||
|
||||
**Changes Made:**
|
||||
1. ✅ Changed import from `forwardRef` to `Ref` type
|
||||
2. ✅ Added `ref?: Ref<HTMLDivElement>` to interface
|
||||
3. ✅ Removed `forwardRef` wrapper
|
||||
4. ✅ Added `ref` to props destructuring
|
||||
5. ✅ Removed `displayName`
|
||||
|
||||
## Example 3: Input with useImperativeHandle
|
||||
|
||||
**Before:**
|
||||
|
||||
```javascript
|
||||
import { forwardRef, useRef, useImperativeHandle } from 'react';
|
||||
|
||||
const SearchInput = forwardRef((props, ref) => {
|
||||
const inputRef = useRef();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus() {
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
clear() {
|
||||
inputRef.current.value = '';
|
||||
},
|
||||
getValue() {
|
||||
return inputRef.current?.value || '';
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default SearchInput;
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```javascript
|
||||
import { useRef, useImperativeHandle } from 'react';
|
||||
|
||||
function SearchInput({ ref, ...props }) {
|
||||
const inputRef = useRef();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus() {
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
clear() {
|
||||
inputRef.current.value = '';
|
||||
},
|
||||
getValue() {
|
||||
return inputRef.current?.value || '';
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchInput;
|
||||
```
|
||||
|
||||
**Usage (unchanged):**
|
||||
|
||||
```javascript
|
||||
function SearchBar() {
|
||||
const searchRef = useRef();
|
||||
|
||||
const handleClear = () => {
|
||||
searchRef.current?.clear();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchInput ref={searchRef} />
|
||||
<button onClick={handleClear}>Clear</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Example 4: Component Library Pattern
|
||||
|
||||
**Before:**
|
||||
|
||||
```typescript
|
||||
import { forwardRef, ComponentPropsWithoutRef, ElementRef } from 'react';
|
||||
|
||||
type ButtonElement = ElementRef<'button'>;
|
||||
type ButtonProps = ComponentPropsWithoutRef<'button'> & {
|
||||
variant?: 'primary' | 'secondary';
|
||||
};
|
||||
|
||||
const Button = forwardRef<ButtonElement, ButtonProps>(
|
||||
({ variant = 'primary', className, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={`btn btn-${variant} ${className || ''}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```typescript
|
||||
import { Ref, ComponentPropsWithoutRef, ElementRef } from 'react';
|
||||
|
||||
type ButtonElement = ElementRef<'button'>;
|
||||
type ButtonProps = ComponentPropsWithoutRef<'button'> & {
|
||||
variant?: 'primary' | 'secondary';
|
||||
ref?: Ref<ButtonElement>;
|
||||
};
|
||||
|
||||
function Button({
|
||||
variant = 'primary',
|
||||
className,
|
||||
ref,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={`btn btn-${variant} ${className || ''}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</examples>
|
||||
|
||||
<constraints>
|
||||
## MUST
|
||||
|
||||
- Add `ref` to props interface when using TypeScript
|
||||
- Use `Ref<HTMLElement>` type from React for TypeScript
|
||||
- Test that ref forwarding works after migration
|
||||
- Maintain component behavior exactly (only syntax changes)
|
||||
|
||||
## SHOULD
|
||||
|
||||
- Migrate components gradually (forwardRef still works)
|
||||
- Update tests to verify ref behavior
|
||||
- Use consistent prop ordering (ref near other element props)
|
||||
- Document breaking changes if part of public API
|
||||
|
||||
## NEVER
|
||||
|
||||
- Remove `forwardRef` if still on React 18
|
||||
- Change component behavior during migration
|
||||
- Break existing ref usage in parent components
|
||||
- Skip TypeScript type updates for ref prop
|
||||
|
||||
</constraints>
|
||||
|
||||
<validation>
|
||||
## After Migration
|
||||
|
||||
1. **Verify Ref Forwarding**:
|
||||
```javascript
|
||||
const ref = useRef(null);
|
||||
<MyComponent ref={ref} />
|
||||
// ref.current should be the DOM element
|
||||
```
|
||||
|
||||
2. **Check TypeScript Compilation**:
|
||||
```bash
|
||||
npx tsc --noEmit
|
||||
```
|
||||
No errors about ref props
|
||||
|
||||
3. **Test Component Behavior**:
|
||||
- Component renders correctly
|
||||
- Ref accesses correct DOM element
|
||||
- useImperativeHandle methods work (if used)
|
||||
- No console warnings about deprecated APIs
|
||||
|
||||
4. **Verify Backward Compatibility**:
|
||||
- Existing usage still works
|
||||
- No breaking changes to component API
|
||||
- Tests pass
|
||||
|
||||
</validation>
|
||||
|
||||
---
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
When migrating a component from forwardRef:
|
||||
|
||||
- [ ] Remove `forwardRef` import
|
||||
- [ ] Remove `forwardRef` wrapper function
|
||||
- [ ] Add `ref` to props destructuring
|
||||
- [ ] Add `ref` type to TypeScript interface (if applicable)
|
||||
- [ ] Remove `displayName` if only used for forwardRef
|
||||
- [ ] Test ref forwarding works
|
||||
- [ ] Update component tests
|
||||
- [ ] Check TypeScript compilation
|
||||
- [ ] Verify no breaking changes to API
|
||||
|
||||
## Common Migration Patterns
|
||||
|
||||
### Pattern 1: Simple Ref Forwarding
|
||||
```javascript
|
||||
// Before
|
||||
const Comp = forwardRef((props, ref) => <div ref={ref} />);
|
||||
|
||||
// After
|
||||
function Comp({ ref }) { return <div ref={ref} />; }
|
||||
```
|
||||
|
||||
### Pattern 2: With useImperativeHandle
|
||||
```javascript
|
||||
// Before
|
||||
const Comp = forwardRef((props, ref) => {
|
||||
useImperativeHandle(ref, () => ({ method() {} }));
|
||||
return <div />;
|
||||
});
|
||||
|
||||
// After
|
||||
function Comp({ ref }) {
|
||||
useImperativeHandle(ref, () => ({ method() {} }));
|
||||
return <div />;
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: TypeScript with Generics
|
||||
```typescript
|
||||
// Before
|
||||
const Comp = forwardRef<HTMLDivElement, Props>((props, ref) => ...);
|
||||
|
||||
// After
|
||||
function Comp({ ref, ...props }: Props & { ref?: Ref<HTMLDivElement> }) { ... }
|
||||
```
|
||||
|
||||
For comprehensive forwardRef migration documentation, see: `research/react-19-comprehensive.md` lines 978-1033.
|
||||
|
||||
## Ref Cleanup Functions (New in React 19)
|
||||
|
||||
React 19 supports cleanup functions in ref callbacks:
|
||||
|
||||
```javascript
|
||||
<div
|
||||
ref={(node) => {
|
||||
console.log('Connected:', node);
|
||||
|
||||
return () => {
|
||||
console.log('Disconnected:', node);
|
||||
};
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
**When Cleanup Runs:**
|
||||
- Component unmounts
|
||||
- Ref changes to different element
|
||||
|
||||
This works with both ref-as-prop and the old forwardRef pattern.
|
||||
80
skills/optimizing-with-react-compiler/SKILL.md
Normal file
80
skills/optimizing-with-react-compiler/SKILL.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: optimizing-with-react-compiler
|
||||
description: Teaches what React Compiler handles automatically in React 19, reducing need for manual memoization. Use when optimizing performance or deciding when to use useMemo/useCallback.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# React Compiler Awareness
|
||||
|
||||
React Compiler (available separately) automatically memoizes code, reducing need for manual optimization. (verify use in project before using this skill)
|
||||
|
||||
## What React Compiler Handles
|
||||
|
||||
**Automatically memoizes:**
|
||||
|
||||
- Component re-renders
|
||||
- Expensive calculations
|
||||
- Function references
|
||||
- Object/array creation
|
||||
|
||||
**Before (Manual Memoization):**
|
||||
|
||||
```javascript
|
||||
function Component({ items }) {
|
||||
const sortedItems = useMemo(() => {
|
||||
return [...items].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [items]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
console.log('Clicked');
|
||||
}, []);
|
||||
|
||||
return <List items={sortedItems} onClick={handleClick} />;
|
||||
}
|
||||
```
|
||||
|
||||
**After (React Compiler):**
|
||||
|
||||
```javascript
|
||||
function Component({ items }) {
|
||||
const sortedItems = [...items].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const handleClick = () => {
|
||||
console.log('Clicked');
|
||||
};
|
||||
|
||||
return <List items={sortedItems} onClick={handleClick} />;
|
||||
}
|
||||
```
|
||||
|
||||
## When Manual Memoization Still Needed
|
||||
|
||||
**Keep `useMemo` when:**
|
||||
|
||||
- Extremely expensive calculations (> 100ms)
|
||||
- Third-party libraries require stable references
|
||||
- React Profiler shows specific performance issues
|
||||
|
||||
**Keep `React.memo` when:**
|
||||
|
||||
- Component re-renders are very expensive
|
||||
- Props rarely change but parent re-renders often
|
||||
- Verified performance improvement with Profiler
|
||||
|
||||
## Performance Best Practices
|
||||
|
||||
**Do:**
|
||||
|
||||
- Trust React Compiler for most optimizations
|
||||
- Keep components small and focused
|
||||
- Keep state local
|
||||
- Use children prop pattern
|
||||
|
||||
**Don't:**
|
||||
|
||||
- Add premature memoization
|
||||
- Over-engineer performance
|
||||
- Skip measuring actual impact
|
||||
|
||||
For comprehensive React Compiler information, see: `research/react-19-comprehensive.md` lines 1179-1223.
|
||||
88
skills/preloading-resources/SKILL.md
Normal file
88
skills/preloading-resources/SKILL.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: preloading-resources
|
||||
description: Teaches resource preloading APIs in React 19 including prefetchDNS, preconnect, preload, and preinit. Use when optimizing initial load or navigation performance.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Resource Preloading APIs
|
||||
|
||||
React 19 adds functions for optimizing resource loading.
|
||||
|
||||
## Available Functions
|
||||
|
||||
```javascript
|
||||
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';
|
||||
```
|
||||
|
||||
### prefetchDNS
|
||||
|
||||
Perform DNS resolution early:
|
||||
|
||||
```javascript
|
||||
function App() {
|
||||
prefetchDNS('https://api.example.com');
|
||||
prefetchDNS('https://cdn.example.com');
|
||||
|
||||
return <div>App content</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### preconnect
|
||||
|
||||
Establish connection before needed:
|
||||
|
||||
```javascript
|
||||
function App() {
|
||||
preconnect('https://api.example.com');
|
||||
|
||||
return <div>App content</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### preload
|
||||
|
||||
Preload resources without executing:
|
||||
|
||||
```javascript
|
||||
function App() {
|
||||
preload('/font.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' });
|
||||
preload('/hero-image.jpg', { as: 'image' });
|
||||
preload('/critical.css', { as: 'style' });
|
||||
|
||||
return <div>App content</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### preinit
|
||||
|
||||
Preload and execute resource:
|
||||
|
||||
```javascript
|
||||
function App() {
|
||||
preinit('/critical.js', { as: 'script' });
|
||||
preinit('/critical.css', { as: 'style', precedence: 'high' });
|
||||
|
||||
return <div>App content</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
**On Route Hover:**
|
||||
```javascript
|
||||
function NavLink({ to, children }) {
|
||||
const handleMouseEnter = () => {
|
||||
preload(`/api/data${to}`, { as: 'fetch' });
|
||||
preinit(`/routes${to}.js`, { as: 'script' });
|
||||
};
|
||||
|
||||
return (
|
||||
<Link to={to} onMouseEnter={handleMouseEnter}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
For comprehensive preloading documentation, see: `research/react-19-comprehensive.md` lines 811-834.
|
||||
42
skills/reviewing-component-architecture/SKILL.md
Normal file
42
skills/reviewing-component-architecture/SKILL.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: reviewing-component-architecture
|
||||
description: Review component architecture for React 19 best practices including size, composition, Server/Client boundaries, and anti-patterns. Use when reviewing component design.
|
||||
review: true
|
||||
allowed-tools: Read, Grep, Glob
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Review: Component Architecture
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Component Size
|
||||
- [ ] Components under 300 lines (break into smaller pieces)
|
||||
- [ ] Single responsibility per component
|
||||
- [ ] No "god components" handling multiple concerns
|
||||
|
||||
### Server vs Client Boundaries
|
||||
- [ ] `'use client'` only where needed (hooks, events, browser APIs)
|
||||
- [ ] Most components are Server Components (smaller bundle)
|
||||
- [ ] Data fetching in Server Components
|
||||
- [ ] No Server Components imported in Client Components
|
||||
|
||||
### Composition Patterns
|
||||
- [ ] Using children prop appropriately
|
||||
- [ ] Compound components for coordinated behavior
|
||||
- [ ] No excessive prop drilling (use Context)
|
||||
- [ ] Composition preferred over complex prop APIs
|
||||
|
||||
### Custom Elements
|
||||
- [ ] Web Components used correctly (no ref workarounds in React 19)
|
||||
- [ ] Custom events use `on + EventName` convention
|
||||
- [ ] Properties vs attributes handled by React
|
||||
|
||||
### Anti-Patterns to Flag
|
||||
- [ ] ❌ God components (> 300 lines, multiple responsibilities)
|
||||
- [ ] ❌ Unnecessary `'use client'` (no hooks/events/browser APIs)
|
||||
- [ ] ❌ Deep prop drilling (3+ levels without Context)
|
||||
- [ ] ❌ Server Components in Client Components
|
||||
- [ ] ❌ Complex component hierarchies (hard to follow)
|
||||
|
||||
For comprehensive component patterns, see: `research/react-19-comprehensive.md`.
|
||||
364
skills/reviewing-hook-patterns/SKILL.md
Normal file
364
skills/reviewing-hook-patterns/SKILL.md
Normal file
@@ -0,0 +1,364 @@
|
||||
---
|
||||
name: reviewing-hook-patterns
|
||||
description: Review React hook usage for React 19 compliance and best practices
|
||||
review: true
|
||||
allowed-tools: Read, Grep, Glob
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Review: React Hook Patterns
|
||||
|
||||
Reviews React hook patterns for React 19 compliance, deprecated APIs, and best practices.
|
||||
|
||||
**Activates:** Code reviews, compliance validation, React 19 migration checks, hook-related PRs
|
||||
|
||||
**Scope:** New hooks (`use()`, `useActionState`, `useOptimistic`), deprecated patterns (forwardRef, propTypes, defaultProps), hook rules, best practices, TypeScript compliance
|
||||
|
||||
**Focus:** Correctness, security (Server Actions), performance (re-renders), React 19 compliance
|
||||
|
||||
## Review Process
|
||||
|
||||
### Phase 1: Find Deprecated APIs
|
||||
|
||||
Use Grep (output_mode: "content"):
|
||||
|
||||
- `forwardRef`
|
||||
- `\.propTypes\s*=` (removed in React 19)
|
||||
- `\.defaultProps\s*=` (deprecated on function components)
|
||||
|
||||
### Phase 2: Validate Hook Usage
|
||||
|
||||
**use() API**
|
||||
|
||||
- ✅ Promises or Context, Suspense-wrapped (Promises), Error Boundary (Promises)
|
||||
- ❌ NOT in try-catch; Promises must be stable (outside component)
|
||||
|
||||
**useActionState**
|
||||
|
||||
- ✅ Server Action receives (previousState, formData), returns serializable values, has error handling, validates inputs server-side
|
||||
- ❌ Missing authentication checks
|
||||
|
||||
**useOptimistic**
|
||||
|
||||
- ✅ Pure update function, paired with startTransition, visual pending indicator
|
||||
- ❌ NOT for critical operations
|
||||
|
||||
**Standard Hooks** (useState, useEffect, etc.)
|
||||
|
||||
- ✅ Top-level calls, all dependencies included, cleanup functions for effects
|
||||
- ❌ Missing dependencies, direct state mutation
|
||||
|
||||
### Phase 3: TypeScript Validation
|
||||
|
||||
**useRef** requires initial value:
|
||||
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
// ❌ Incorrect (React 19)
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
```
|
||||
|
||||
**Ref as prop** typed correctly:
|
||||
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
interface Props {
|
||||
ref?: Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
// ❌ Incorrect (using forwardRef)
|
||||
const Comp = forwardRef<HTMLButtonElement, Props>(...);
|
||||
```
|
||||
|
||||
### Phase 4: Identify Anti-Patterns
|
||||
|
||||
**Array index as key:**
|
||||
|
||||
```javascript
|
||||
// ❌ Bad
|
||||
{
|
||||
items.map((item, index) => <div key={index}>{item}</div>);
|
||||
}
|
||||
|
||||
// ✅ Good
|
||||
{
|
||||
items.map((item) => <div key={item.id}>{item}</div>);
|
||||
}
|
||||
```
|
||||
|
||||
**Direct state mutation:**
|
||||
|
||||
```javascript
|
||||
// ❌ Bad
|
||||
const [items, setItems] = useState([]);
|
||||
items.push(newItem);
|
||||
setItems(items);
|
||||
|
||||
// ✅ Good
|
||||
setItems([...items, newItem]);
|
||||
```
|
||||
|
||||
**Missing dependencies:**
|
||||
|
||||
```javascript
|
||||
// ❌ Bad
|
||||
useEffect(() => {
|
||||
fetchData(userId);
|
||||
}, []);
|
||||
|
||||
// ✅ Good
|
||||
useEffect(() => {
|
||||
fetchData(userId);
|
||||
}, [userId]);
|
||||
```
|
||||
|
||||
**Missing cleanup:**
|
||||
|
||||
```javascript
|
||||
// ❌ Bad
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {}, 1000);
|
||||
}, []);
|
||||
|
||||
// ✅ Good
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
```
|
||||
|
||||
## Report Format
|
||||
|
||||
### ✅ Compliant Patterns
|
||||
|
||||
List correct React 19 usage, good practices, proper hooks.
|
||||
|
||||
### ⚠️ Warnings (Non-blocking)
|
||||
|
||||
Deprecated APIs still functional but require migration: forwardRef, manual memoization (when React Compiler available), patterns with better React 19 alternatives.
|
||||
|
||||
### ❌ Issues (Must Fix)
|
||||
|
||||
- **Removed APIs:** propTypes, defaultProps on function components, string refs
|
||||
- **Security:** Unvalidated Server Actions, missing authentication, XSS vulnerabilities
|
||||
- **Hook Rules:** Conditional calls, missing dependencies, hooks outside components
|
||||
|
||||
### 📝 Recommendations
|
||||
|
||||
Migration paths, performance improvements, best practices, relevant skill references.
|
||||
|
||||
## Example Review: Form Component
|
||||
|
||||
**Code:**
|
||||
|
||||
```javascript
|
||||
import { useState } from 'react';
|
||||
|
||||
function ContactForm() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, message }),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<textarea value={message} onChange={(e) => setMessage(e.target.value)} />
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Issues
|
||||
|
||||
1. **Missing Server Action Pattern** — Use `useActionState` with server validation instead of client-side fetch
|
||||
2. **No Validation** — Add server-side validation (security risk)
|
||||
3. **No Loading State** — Use `isPending` for UX feedback
|
||||
4. **No Error Handling** — Return error state from Server Action
|
||||
|
||||
### 📝 Corrected Implementation
|
||||
|
||||
```javascript
|
||||
'use client';
|
||||
|
||||
import { useActionState } from 'react';
|
||||
import { submitContact } from './actions';
|
||||
|
||||
function ContactForm() {
|
||||
const [state, formAction, isPending] = useActionState(submitContact, null);
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<input name="email" type="email" required />
|
||||
<textarea name="message" required />
|
||||
<button type="submit" disabled={isPending}>
|
||||
{isPending ? 'Sending...' : 'Send'}
|
||||
</button>
|
||||
{state?.error && <p className="error">{state.error}</p>}
|
||||
{state?.success && <p>Message sent!</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Server Action (actions.js):**
|
||||
|
||||
```javascript
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
message: z.string().min(10),
|
||||
});
|
||||
|
||||
export async function submitContact(previousState, formData) {
|
||||
const data = {
|
||||
email: formData.get('email'),
|
||||
message: formData.get('message'),
|
||||
};
|
||||
|
||||
const result = schema.safeParse(data);
|
||||
if (!result.success) return { error: 'Invalid input' };
|
||||
|
||||
try {
|
||||
await db.contacts.create({ data: result.data });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { error: 'Failed to send message' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example Review: forwardRef Component
|
||||
|
||||
**Code:**
|
||||
|
||||
```javascript
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
const Button = forwardRef((props, ref) => (
|
||||
<button ref={ref} {...props}>
|
||||
{props.children}
|
||||
</button>
|
||||
));
|
||||
|
||||
Button.displayName = 'Button';
|
||||
```
|
||||
|
||||
### ⚠️ Warnings
|
||||
|
||||
**Deprecated forwardRef Usage** — Still functional in React 19 but deprecated. Migrate to ref-as-prop pattern.
|
||||
|
||||
### 📝 React 19 Migration
|
||||
|
||||
```javascript
|
||||
function Button({ children, ref, ...props }) {
|
||||
return (
|
||||
<button ref={ref} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:** Simpler API, better TypeScript inference, follows React 19 patterns, less boilerplate.
|
||||
|
||||
## Review Standards
|
||||
|
||||
**MUST Flag:**
|
||||
|
||||
- Removed APIs (propTypes, defaultProps on functions, string refs)
|
||||
- Hook rule violations (conditional calls, missing dependencies)
|
||||
- Security issues (unvalidated Server Actions, missing auth)
|
||||
- Missing Suspense/Error Boundaries for `use()` with Promises
|
||||
|
||||
**SHOULD Flag:**
|
||||
|
||||
- Deprecated APIs (forwardRef)
|
||||
- Performance issues (unnecessary re-renders, missing memoization when needed)
|
||||
- Missing TypeScript types for React 19 patterns
|
||||
- Anti-patterns (array index keys, direct state mutation)
|
||||
|
||||
**MAY Suggest:**
|
||||
|
||||
- Better React 19 patterns available
|
||||
- Component architecture improvements
|
||||
- Code organization enhancements
|
||||
|
||||
**NEVER:**
|
||||
|
||||
- Enforce personal style preferences
|
||||
- Require changes that don't improve correctness/security/performance
|
||||
- Flag patterns working correctly in React 19
|
||||
- Demand premature optimization
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### New React 19 Features
|
||||
|
||||
- [ ] `use()` + Promises: Suspense + Error Boundary
|
||||
- [ ] `use()` + Context: appropriate usage
|
||||
- [ ] `useActionState`: Server Actions validate inputs
|
||||
- [ ] `useOptimistic`: paired with `startTransition`
|
||||
- [ ] `useFormStatus`: inside form components
|
||||
|
||||
### Deprecated Patterns
|
||||
|
||||
- [ ] No forwardRef or flagged for migration
|
||||
- [ ] No propTypes on function components
|
||||
- [ ] No defaultProps on function components
|
||||
- [ ] No string refs
|
||||
|
||||
### Hook Rules
|
||||
|
||||
- [ ] All hooks at top level
|
||||
- [ ] No conditional hook calls
|
||||
- [ ] All dependencies included
|
||||
- [ ] Cleanup functions for subscriptions
|
||||
|
||||
### TypeScript (if applicable)
|
||||
|
||||
- [ ] `useRef` has initial value
|
||||
- [ ] Ref props typed with `Ref<HTMLElement>`
|
||||
- [ ] Server Actions properly typed
|
||||
- [ ] No deprecated type patterns
|
||||
|
||||
### Security
|
||||
|
||||
- [ ] Server Actions validate inputs
|
||||
- [ ] Authentication checks present where needed
|
||||
- [ ] `dangerouslySetInnerHTML` sanitized
|
||||
- [ ] No sensitive data exposed to client
|
||||
|
||||
### Performance
|
||||
|
||||
- [ ] No array index as key
|
||||
- [ ] No direct state mutation
|
||||
- [ ] No missing dependencies (stale closures)
|
||||
- [ ] Reasonable component structure
|
||||
|
||||
## Common Issues Reference
|
||||
|
||||
| Issue | Search Pattern | Fix |
|
||||
| --------------------------- | -------------------- | ----------------------- |
|
||||
| forwardRef usage | `forwardRef` | Convert to ref-as-prop |
|
||||
| propTypes | `\.propTypes\s*=` | Remove (use TypeScript) |
|
||||
| defaultProps | `\.defaultProps\s*=` | Use ES6 defaults |
|
||||
| Missing dependencies | Manual review | Add to dependency array |
|
||||
| Array index keys | `key={.*index}` | Use stable ID |
|
||||
| Direct mutation | Manual review | Use immutable updates |
|
||||
| use() without Suspense | Manual review | Add Suspense boundary |
|
||||
| Server Action no validation | Manual review | Add zod/yup validation |
|
||||
|
||||
For comprehensive React 19 patterns and migration guides, see: `research/react-19-comprehensive.md`
|
||||
43
skills/reviewing-performance-patterns/SKILL.md
Normal file
43
skills/reviewing-performance-patterns/SKILL.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: reviewing-performance-patterns
|
||||
description: Review React 19 performance patterns including memoization, re-renders, and bundle size. Use when reviewing performance or optimization.
|
||||
review: true
|
||||
allowed-tools: Read, Grep
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Review: Performance Patterns
|
||||
|
||||
## Checklist
|
||||
|
||||
### Re-Rendering
|
||||
- [ ] No unnecessary re-renders from prop changes
|
||||
- [ ] Using React Compiler when possible (reduces manual memoization)
|
||||
- [ ] Context split for different concerns (avoid re-render cascades)
|
||||
- [ ] Children prop pattern to prevent wrapper re-renders
|
||||
|
||||
### Memoization
|
||||
- [ ] Manual `useMemo`/`useCallback` only when needed (React Compiler handles most cases)
|
||||
- [ ] `React.memo` used selectively for expensive components
|
||||
- [ ] Dependencies correct in memoization hooks
|
||||
- [ ] Not over-optimizing (premature optimization)
|
||||
|
||||
### Bundle Size
|
||||
- [ ] Code splitting used for heavy components/routes
|
||||
- [ ] Server Components used where appropriate (zero client JS)
|
||||
- [ ] `'use client'` only where needed
|
||||
- [ ] Lazy loading for non-critical components
|
||||
|
||||
### Resource Loading
|
||||
- [ ] Using preload/preinit for critical resources
|
||||
- [ ] DNS prefetch for external domains
|
||||
- [ ] Images optimized and lazy loaded
|
||||
- [ ] Fonts preloaded
|
||||
|
||||
### Anti-Patterns
|
||||
- [ ] ❌ Array index as key (causes unnecessary re-renders)
|
||||
- [ ] ❌ Creating new objects/arrays in render (breaks memoization)
|
||||
- [ ] ❌ Excessive memoization without measurement
|
||||
- [ ] ❌ Unnecessary `'use client'` (increases bundle)
|
||||
|
||||
For comprehensive performance patterns, see: `research/react-19-comprehensive.md`.
|
||||
67
skills/reviewing-server-actions/SKILL.md
Normal file
67
skills/reviewing-server-actions/SKILL.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: reviewing-server-actions
|
||||
description: Review Server Actions for security, validation, and best practices in React 19. Use when reviewing forms, mutations, or server-side logic.
|
||||
review: true
|
||||
allowed-tools: Read, Grep, Glob
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Review: Server Actions
|
||||
|
||||
## Security Checklist
|
||||
|
||||
### Input Validation
|
||||
- [ ] All inputs validated with schema (zod, yup, etc.)
|
||||
- [ ] Type coercion handled correctly (FormData.get returns strings)
|
||||
- [ ] Length limits enforced
|
||||
- [ ] No SQL injection vulnerabilities
|
||||
|
||||
For runtime validation patterns and type safety, use the using-runtime-checks skill from the typescript plugin.
|
||||
|
||||
If reviewing Zod schema validation patterns, use the validating-schema-basics skill for type-safe Zod v4 schema patterns.
|
||||
|
||||
### Authentication & Authorization
|
||||
- [ ] Session/auth checked before mutations
|
||||
- [ ] User permissions verified
|
||||
- [ ] Resource ownership validated
|
||||
- [ ] No unauthorized access possible
|
||||
|
||||
For secure credential handling, use the SECURITY-credentials skill from the typescript plugin.
|
||||
|
||||
### Data Sanitization
|
||||
- [ ] User input sanitized before storage
|
||||
- [ ] No XSS vulnerabilities
|
||||
- [ ] File uploads validated (type, size, content)
|
||||
- [ ] Dangerous operations require confirmation
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Error Handling
|
||||
- [ ] Try-catch blocks for async operations
|
||||
- [ ] Specific error messages for users
|
||||
- [ ] No sensitive data in error messages
|
||||
- [ ] Logging for debugging
|
||||
|
||||
### Return Values
|
||||
- [ ] Return serializable objects only
|
||||
- [ ] Consistent response format
|
||||
- [ ] Success and error states handled
|
||||
- [ ] Field-specific errors when needed
|
||||
|
||||
### Performance
|
||||
- [ ] Database queries optimized
|
||||
- [ ] No N+1 query problems
|
||||
- [ ] Appropriate use of transactions
|
||||
- [ ] Rate limiting where needed
|
||||
|
||||
## Anti-Patterns to Flag
|
||||
|
||||
- [ ] ❌ No validation (trusting client input)
|
||||
- [ ] ❌ No authentication checks
|
||||
- [ ] ❌ Returning non-serializable values (functions, classes)
|
||||
- [ ] ❌ Missing error handling
|
||||
- [ ] ❌ Exposing sensitive data
|
||||
- [ ] ❌ Direct database queries without sanitization
|
||||
- [ ] ❌ No rate limiting on critical actions
|
||||
|
||||
For comprehensive Server Actions security, see: `research/react-19-comprehensive.md` lines 723-729, 1808-1942.
|
||||
43
skills/reviewing-state-management/SKILL.md
Normal file
43
skills/reviewing-state-management/SKILL.md
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: reviewing-state-management
|
||||
description: Review state management patterns for React 19 best practices. Use when reviewing component state, Context usage, or state architecture.
|
||||
review: true
|
||||
allowed-tools: Read, Grep
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Review: State Management
|
||||
|
||||
## Checklist
|
||||
|
||||
### State Immutability
|
||||
- [ ] No direct state mutation
|
||||
- [ ] Using spread operators or immutable update patterns
|
||||
- [ ] Arrays updated with map/filter/concat (not push/splice)
|
||||
- [ ] Objects updated with spread or Object.assign
|
||||
|
||||
### State Location
|
||||
- [ ] Local state used when appropriate
|
||||
- [ ] Context only for cross-cutting concerns
|
||||
- [ ] No prop drilling through 3+ levels
|
||||
- [ ] Frequently changing state not in Context
|
||||
|
||||
### useReducer Usage
|
||||
- [ ] Used for complex state logic
|
||||
- [ ] Reducer functions are pure
|
||||
- [ ] Action types are consistent
|
||||
- [ ] State updates follow patterns
|
||||
|
||||
### Context Patterns
|
||||
- [ ] Split contexts for different concerns
|
||||
- [ ] Using `use()` API in React 19 (not `useContext`)
|
||||
- [ ] Context providers at appropriate level
|
||||
- [ ] No unnecessary re-renders
|
||||
|
||||
### Anti-Patterns
|
||||
- [ ] ❌ Direct state mutation (`state.push()`, `state.x = y`)
|
||||
- [ ] ❌ Context for frequently changing values
|
||||
- [ ] ❌ Excessive prop drilling
|
||||
- [ ] ❌ God components managing too much state
|
||||
|
||||
For comprehensive state patterns, see: `research/react-19-comprehensive.md`.
|
||||
52
skills/reviewing-test-quality/SKILL.md
Normal file
52
skills/reviewing-test-quality/SKILL.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: reviewing-test-quality
|
||||
description: Review React 19 test quality including coverage, patterns, and React 19 API testing. Use when reviewing tests or test coverage.
|
||||
review: true
|
||||
allowed-tools: Read, Grep
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Review: Test Quality
|
||||
|
||||
For Vitest configuration validation (pool options, coverage setup, deprecated patterns), see `vitest-4/skills/reviewing-vitest-config/SKILL.md`.
|
||||
|
||||
## Checklist
|
||||
|
||||
### Test Coverage
|
||||
- [ ] Components have tests for user interactions
|
||||
- [ ] Forms test both success and error paths
|
||||
- [ ] Server Actions tested in isolation
|
||||
- [ ] Custom hooks tested with `renderHook`
|
||||
- [ ] Edge cases covered (empty states, errors, loading)
|
||||
|
||||
### React 19 APIs
|
||||
- [ ] Forms using `useActionState` have tests
|
||||
- [ ] `useOptimistic` updates tested for immediate feedback
|
||||
- [ ] `useFormStatus` tested within form component context
|
||||
- [ ] Server Actions tested with mocked auth/database
|
||||
- [ ] `use()` hook tested with Promises and Context
|
||||
|
||||
### Testing Patterns
|
||||
- [ ] Using `@testing-library/react` and `@testing-library/user-event`
|
||||
- [ ] Queries prefer accessibility (`getByRole`, `getByLabelText`)
|
||||
- [ ] `waitFor` used for async assertions
|
||||
- [ ] Mocking external dependencies (API, database)
|
||||
- [ ] Tests are isolated and don't depend on each other
|
||||
|
||||
### Anti-Patterns
|
||||
- [ ] ❌ Testing implementation details (internal state, methods)
|
||||
- [ ] ❌ Querying by class names or data-testid when role available
|
||||
- [ ] ❌ Not waiting for async updates (`waitFor`)
|
||||
- [ ] ❌ Testing components without user interactions
|
||||
- [ ] ❌ Missing error case tests
|
||||
|
||||
### Server Action Testing
|
||||
- [ ] Server Actions tested as functions (not through UI)
|
||||
- [ ] Input validation tested
|
||||
- [ ] Authentication/authorization tested
|
||||
- [ ] Database operations mocked
|
||||
- [ ] Error handling tested
|
||||
|
||||
For typed test fixtures and mocks, use the TYPES-generics skill from the typescript plugin.
|
||||
|
||||
For comprehensive testing patterns, see: `research/react-19-comprehensive.md`.
|
||||
213
skills/supporting-custom-elements/SKILL.md
Normal file
213
skills/supporting-custom-elements/SKILL.md
Normal file
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: supporting-custom-elements
|
||||
description: Teaches Web Components (Custom Elements) support in React 19, including property vs attribute handling and custom events. Use when integrating Web Components or working with custom HTML elements.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Web Components Support in React 19
|
||||
|
||||
<role>
|
||||
This skill teaches you how to use Web Components (Custom Elements) in React 19, which now has full support.
|
||||
</role>
|
||||
|
||||
<when-to-activate>
|
||||
This skill activates when:
|
||||
|
||||
- Working with Web Components or Custom Elements
|
||||
- Integrating third-party web components
|
||||
- Passing props to custom elements
|
||||
- Handling custom events from web components
|
||||
- Migration from React 18 web component workarounds
|
||||
</when-to-activate>
|
||||
|
||||
<overview>
|
||||
React 19 adds full support for Custom Elements and passes all Custom Elements Everywhere tests.
|
||||
|
||||
**New in React 19:**
|
||||
|
||||
1. **Automatic Property Detection** - React determines property vs attribute
|
||||
2. **Custom Event Support** - Standard `on + EventName` convention
|
||||
3. **Boolean Attributes** - Properly handled (added/removed as needed)
|
||||
4. **No Ref Workarounds** - Pass props directly to custom elements
|
||||
|
||||
**Before React 19:**
|
||||
|
||||
```javascript
|
||||
<web-counter
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
el.increment = increment;
|
||||
el.isDark = isDark;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
**React 19:**
|
||||
|
||||
```javascript
|
||||
<web-counter increment={increment} isDark={isDark} onIncrementEvent={() => setCount(count + 1)} />
|
||||
```
|
||||
|
||||
</overview>
|
||||
|
||||
<workflow>
|
||||
## Using Web Components
|
||||
|
||||
**Step 1: Define Custom Element** (or use third-party)
|
||||
|
||||
```javascript
|
||||
class WebCounter extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
set increment(fn) {
|
||||
this._increment = fn;
|
||||
}
|
||||
|
||||
set isDark(value) {
|
||||
this._isDark = value;
|
||||
this.render();
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
this._increment?.();
|
||||
this.dispatchEvent(new CustomEvent('incremented'));
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
<button style="color: ${this._isDark ? 'white' : 'black'}">
|
||||
Increment
|
||||
</button>
|
||||
`;
|
||||
this.querySelector('button').onclick = () => this.handleClick();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('web-counter', WebCounter);
|
||||
```
|
||||
|
||||
**Step 2: Use in React 19**
|
||||
|
||||
```javascript
|
||||
function App() {
|
||||
const [count, setCount] = useState(0);
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
const increment = () => setCount(count + 1);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Count: {count}</p>
|
||||
|
||||
<web-counter
|
||||
increment={increment}
|
||||
isDark={isDark}
|
||||
onIncremented={() => console.log('Incremented!')}
|
||||
/>
|
||||
|
||||
<button onClick={() => setIsDark(!isDark)}>Toggle Theme</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Handle Custom Events**
|
||||
|
||||
Custom events follow `on + EventName` convention:
|
||||
|
||||
```javascript
|
||||
<my-button label="Click Me" onButtonClick={handleClick} />
|
||||
```
|
||||
|
||||
If custom element dispatches `buttonClick` event, React automatically wires it up.
|
||||
|
||||
</workflow>
|
||||
|
||||
<examples>
|
||||
## Example: Third-Party Web Component
|
||||
|
||||
```javascript
|
||||
import '@material/mwc-button';
|
||||
|
||||
function MaterialButton() {
|
||||
return <mwc-button raised label="Click me" icon="code" onClick={() => alert('Clicked!')} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Custom Form Element
|
||||
|
||||
```javascript
|
||||
class RatingInput extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.rating = 0;
|
||||
this.render();
|
||||
}
|
||||
|
||||
setRating(value) {
|
||||
this.rating = value;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('ratingChange', {
|
||||
detail: { rating: value },
|
||||
})
|
||||
);
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
${[1, 2, 3, 4, 5]
|
||||
.map(
|
||||
(i) => `
|
||||
<button data-rating="${i}">⭐</button>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
`;
|
||||
|
||||
this.querySelectorAll('button').forEach((btn) => {
|
||||
btn.onclick = () => this.setRating(+btn.dataset.rating);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('rating-input', RatingInput);
|
||||
```
|
||||
|
||||
```javascript
|
||||
function ReviewForm() {
|
||||
const [rating, setRating] = useState(0);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<rating-input onRatingChange={(e) => setRating(e.detail.rating)} />
|
||||
<p>Rating: {rating}</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
For comprehensive Custom Elements documentation, see: `research/react-19-comprehensive.md` lines 1034-1089.
|
||||
</examples>
|
||||
|
||||
<constraints>
|
||||
## MUST
|
||||
- Use standard `on + EventName` for custom events
|
||||
- Let React determine property vs attribute automatically
|
||||
- Define custom elements before using in React
|
||||
|
||||
## SHOULD
|
||||
|
||||
- Prefer Web Components for framework-agnostic widgets
|
||||
- Use TypeScript declarations for custom elements
|
||||
- Test SSR vs CSR rendering differences
|
||||
|
||||
## NEVER
|
||||
|
||||
- Use ref workarounds (React 19 handles props directly)
|
||||
- Forget to define custom elements (will render as unknown tag)
|
||||
- Pass non-primitive values in SSR context (will be omitted)
|
||||
</constraints>
|
||||
92
skills/testing-components/SKILL.md
Normal file
92
skills/testing-components/SKILL.md
Normal file
@@ -0,0 +1,92 @@
|
||||
---
|
||||
name: testing-components
|
||||
description: Teaches React Testing Library patterns for React 19 components. Use when writing component tests, testing interactions, or testing with Server Actions.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Testing React 19 Components
|
||||
|
||||
For Vitest test structure and mocking patterns (describe/test blocks, vi.fn(), assertions), see `vitest-4/skills/writing-vitest-tests/SKILL.md`.
|
||||
|
||||
## Basic Component Testing
|
||||
|
||||
```javascript
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import Button from './Button';
|
||||
|
||||
test('button renders and handles click', async () => {
|
||||
const handleClick = vi.fn();
|
||||
|
||||
render(<Button onClick={handleClick}>Click me</Button>);
|
||||
|
||||
const button = screen.getByRole('button', { name: /click me/i });
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(button);
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Forms with useActionState
|
||||
|
||||
```javascript
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import ContactForm from './ContactForm';
|
||||
|
||||
vi.mock('./actions', () => ({
|
||||
submitContact: vi.fn(async (prev, formData) => {
|
||||
const email = formData.get('email');
|
||||
if (!email?.includes('@')) {
|
||||
return { error: 'Invalid email' };
|
||||
}
|
||||
return { success: true };
|
||||
}),
|
||||
}));
|
||||
|
||||
test('form shows error for invalid email', async () => {
|
||||
render(<ContactForm />);
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/email/i), 'invalid');
|
||||
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/invalid email/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('form succeeds with valid email', async () => {
|
||||
render(<ContactForm />);
|
||||
|
||||
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
|
||||
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/success/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Testing with Context
|
||||
|
||||
```javascript
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { UserProvider } from './UserContext';
|
||||
import UserProfile from './UserProfile';
|
||||
|
||||
test('displays user name from context', () => {
|
||||
const user = { name: 'Alice', email: 'alice@example.com' };
|
||||
|
||||
render(
|
||||
<UserProvider value={user}>
|
||||
<UserProfile />
|
||||
</UserProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Alice')).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
For comprehensive testing patterns, see: React Testing Library documentation.
|
||||
72
skills/testing-hooks/SKILL.md
Normal file
72
skills/testing-hooks/SKILL.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
name: testing-hooks
|
||||
description: Teaches testing custom hooks in React 19 using renderHook from React Testing Library. Use when testing custom hooks or hook behavior.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Testing Custom Hooks
|
||||
|
||||
## Basic Hook Testing
|
||||
|
||||
For hook testing with Vitest fixtures and describe/test patterns, use vitest-4/skills/writing-vitest-tests which covers test structure and fixture patterns.
|
||||
|
||||
```javascript
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useCounter } from './useCounter';
|
||||
|
||||
test('useCounter increments', () => {
|
||||
const { result } = renderHook(() => useCounter());
|
||||
|
||||
expect(result.current.count).toBe(0);
|
||||
|
||||
act(() => {
|
||||
result.current.increment();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Hooks with Props
|
||||
|
||||
```javascript
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useUser } from './useUser';
|
||||
|
||||
test('useUser fetches user data', async () => {
|
||||
const { result } = renderHook(() => useUser('123'));
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.user).toEqual({ id: '123', name: 'Alice' });
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Hooks with Context
|
||||
|
||||
```javascript
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { ThemeProvider } from './ThemeContext';
|
||||
import { useTheme } from './useTheme';
|
||||
|
||||
test('useTheme returns theme from context', () => {
|
||||
const wrapper = ({ children }) => (
|
||||
<ThemeProvider value="dark">{children}</ThemeProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useTheme(), { wrapper });
|
||||
|
||||
expect(result.current.theme).toBe('dark');
|
||||
});
|
||||
```
|
||||
|
||||
For comprehensive hook testing patterns, see: React Testing Library documentation.
|
||||
|
||||
## References
|
||||
|
||||
- [@vitest-4/skills/writing-vitest-tests](/vitest-4/skills/writing-vitest-tests/SKILL.md) - Fixture patterns and setup
|
||||
96
skills/testing-server-actions/SKILL.md
Normal file
96
skills/testing-server-actions/SKILL.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
name: testing-server-actions
|
||||
description: Teaches testing Server Actions in isolation in React 19. Use when testing Server Actions, form handling, or server-side logic.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Testing Server Actions
|
||||
|
||||
For Vitest mocking patterns (vi.mock(), vi.fn(), mockResolvedValue), see `vitest-4/skills/writing-vitest-tests/SKILL.md`.
|
||||
|
||||
## Basic Server Action Test
|
||||
|
||||
```javascript
|
||||
import { submitContact } from './actions';
|
||||
|
||||
test('submitContact validates email', async () => {
|
||||
const formData = new FormData();
|
||||
formData.set('email', 'invalid');
|
||||
formData.set('message', 'Hello');
|
||||
|
||||
const result = await submitContact(null, formData);
|
||||
|
||||
expect(result.error).toBeTruthy();
|
||||
});
|
||||
|
||||
test('submitContact succeeds with valid data', async () => {
|
||||
const formData = new FormData();
|
||||
formData.set('email', 'test@example.com');
|
||||
formData.set('message', 'Hello');
|
||||
|
||||
const result = await submitContact(null, formData);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
## Mocking Database Calls
|
||||
|
||||
```javascript
|
||||
import { createUser } from './actions';
|
||||
import { db } from './db';
|
||||
|
||||
vi.mock('./db', () => ({
|
||||
db: {
|
||||
users: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
test('createUser creates database record', async () => {
|
||||
db.users.create.mockResolvedValue({ id: '123', name: 'Alice' });
|
||||
|
||||
const formData = new FormData();
|
||||
formData.set('name', 'Alice');
|
||||
formData.set('email', 'alice@example.com');
|
||||
|
||||
const result = await createUser(null, formData);
|
||||
|
||||
expect(db.users.create).toHaveBeenCalledWith({
|
||||
name: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.userId).toBe('123');
|
||||
});
|
||||
```
|
||||
|
||||
## Testing with Authentication
|
||||
|
||||
```javascript
|
||||
import { deletePost } from './actions';
|
||||
import { getSession } from './auth';
|
||||
|
||||
vi.mock('./auth');
|
||||
|
||||
test('deletePost requires authentication', async () => {
|
||||
getSession.mockResolvedValue(null);
|
||||
|
||||
await expect(deletePost('post-123')).rejects.toThrow('Unauthorized');
|
||||
});
|
||||
|
||||
test('deletePost checks ownership', async () => {
|
||||
getSession.mockResolvedValue({ user: { id: 'user-1' } });
|
||||
|
||||
await expect(deletePost('post-owned-by-user-2')).rejects.toThrow('Forbidden');
|
||||
});
|
||||
```
|
||||
|
||||
For comprehensive Server Action testing, test the function directly in isolation.
|
||||
|
||||
## References
|
||||
|
||||
- [@vitest-4/skills/writing-vitest-tests](/vitest-4/skills/writing-vitest-tests/SKILL.md) - Mocking and spy patterns
|
||||
79
skills/tracking-form-status/SKILL.md
Normal file
79
skills/tracking-form-status/SKILL.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: tracking-form-status
|
||||
description: Teaches useFormStatus hook for tracking form submission state in React 19. Use when implementing submit buttons, form loading states, or pending indicators.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Form Status Tracking with useFormStatus
|
||||
|
||||
`useFormStatus` provides status info about parent form submissions.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
**MUST be called inside a form component:**
|
||||
|
||||
```javascript
|
||||
import { useFormStatus } from 'react-dom';
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending, data, method, action } = useFormStatus();
|
||||
|
||||
return (
|
||||
<button type="submit" disabled={pending}>
|
||||
{pending ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function MyForm() {
|
||||
async function handleSubmit(formData) {
|
||||
'use server';
|
||||
await saveData(formData);
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={handleSubmit}>
|
||||
<input name="email" />
|
||||
<SubmitButton />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Return Values
|
||||
|
||||
- `pending` (boolean): Whether form is submitting
|
||||
- `data` (FormData | null): Data being submitted
|
||||
- `method` (string): HTTP method ('get' or 'post')
|
||||
- `action` (function | null): Action function
|
||||
|
||||
## Critical Requirement
|
||||
|
||||
❌ **Wrong - called at form level:**
|
||||
|
||||
```javascript
|
||||
function MyForm() {
|
||||
const { pending } = useFormStatus();
|
||||
return <form>...</form>;
|
||||
}
|
||||
```
|
||||
|
||||
✅ **Correct - called inside form:**
|
||||
|
||||
```javascript
|
||||
function MyForm() {
|
||||
return (
|
||||
<form>
|
||||
<SubmitButton />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
return <button disabled={pending}>Submit</button>;
|
||||
}
|
||||
```
|
||||
|
||||
For comprehensive useFormStatus documentation, see: `research/react-19-comprehensive.md` lines 312-355.
|
||||
293
skills/using-action-state/SKILL.md
Normal file
293
skills/using-action-state/SKILL.md
Normal file
@@ -0,0 +1,293 @@
|
||||
---
|
||||
name: using-action-state
|
||||
description: Teaches useActionState hook for managing form state with Server Actions in React 19. Use when implementing forms, handling form submissions, tracking pending states, or working with Server Functions.
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# useActionState Patterns for Forms
|
||||
|
||||
<role>
|
||||
Teaches React 19's `useActionState` hook for form state management with Server Actions.
|
||||
</role>
|
||||
|
||||
<when-to-activate>
|
||||
When: mentioning `useActionState`, form state/handling, Server Actions/Functions; tracking pending/submission status; implementing progressive enhancement; server-side form validation.
|
||||
</when-to-activate>
|
||||
|
||||
<overview>
|
||||
`useActionState` manages form state based on action results: tracks pending state (automatic `isPending`), manages form state (returns action results), integrates Server Actions (`'use server'`), enables progressive enhancement (optional no-JS permalink). Replaces manual form submission state management.
|
||||
</overview>
|
||||
|
||||
<workflow>
|
||||
## Standard Form with useActionState
|
||||
|
||||
**Server Action:**
|
||||
|
||||
```javascript
|
||||
'use server';
|
||||
|
||||
export async function submitForm(previousState, formData) {
|
||||
const email = formData.get('email');
|
||||
|
||||
if (!email || !email.includes('@')) {
|
||||
return { error: 'Invalid email address' };
|
||||
}
|
||||
|
||||
await saveToDatabase({ email });
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
**Component:**
|
||||
|
||||
```javascript
|
||||
'use client';
|
||||
|
||||
import { useActionState } from 'react';
|
||||
import { submitForm } from './actions';
|
||||
|
||||
function ContactForm() {
|
||||
const [state, formAction, isPending] = useActionState(submitForm, null);
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<input name="email" type="email" required />
|
||||
|
||||
<button type="submit" disabled={isPending}>
|
||||
{isPending ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
|
||||
{state?.error && <p className="error">{state.error}</p>}
|
||||
{state?.success && <p className="success">Submitted!</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</workflow>
|
||||
|
||||
<conditional-workflows>
|
||||
## Decision Points
|
||||
|
||||
**Progressive Enhancement:** Add permalink as third argument: `useActionState(submitForm, null, '/api/submit')`. Form submits to URL before JS loads; server handles both cases.
|
||||
|
||||
**Validation:** Server Action receives previousState, returns error object for failures, success object when valid; component renders errors from state.
|
||||
|
||||
**Multi-Step Forms:** Track step in state; Server Action advances step or returns errors; component renders current step.
|
||||
</conditional-workflows>
|
||||
|
||||
<progressive-disclosure>
|
||||
## References
|
||||
|
||||
- **Server Actions**: `../../forms/skills/server-actions/SKILL.md`
|
||||
- **Form Validation**: `../../forms/skills/form-validation/SKILL.md`
|
||||
- **Progressive Enhancement**: `../../../research/react-19-comprehensive.md` (lines 715-722)
|
||||
|
||||
**Cross-Plugin References:**
|
||||
|
||||
- If customizing validation error messages, use the customizing-errors skill for error formatting with safeParse and field error flattening
|
||||
|
||||
Load as needed for specific patterns.
|
||||
</progressive-disclosure>
|
||||
|
||||
<examples>
|
||||
## Example 1: Validation with
|
||||
|
||||
Zod
|
||||
|
||||
```javascript
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
message: z.string().min(10).max(1000),
|
||||
});
|
||||
|
||||
export async function contactAction(previousState, formData) {
|
||||
const data = {
|
||||
email: formData.get('email'),
|
||||
message: formData.get('message'),
|
||||
};
|
||||
|
||||
const result = schema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
return { errors: result.error.flatten().fieldErrors };
|
||||
}
|
||||
|
||||
try {
|
||||
await db.contacts.create({ data: result.data });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { error: 'Failed to submit contact form' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
'use client';
|
||||
|
||||
import { useActionState } from 'react';
|
||||
import { contactAction } from './actions';
|
||||
|
||||
export default function ContactForm() {
|
||||
const [state, formAction, isPending] = useActionState(contactAction, null);
|
||||
|
||||
if (state?.success) {
|
||||
return <p>Thank you for contacting us!</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input id="email" name="email" type="email" required />
|
||||
{state?.errors?.email && <span className="error">{state.errors.email}</span>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message">Message</label>
|
||||
<textarea id="message" name="message" required />
|
||||
{state?.errors?.message && <span className="error">{state.errors.message}</span>}
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={isPending}>
|
||||
{isPending ? 'Sending...' : 'Send Message'}
|
||||
</button>
|
||||
|
||||
{state?.error && <p className="error">{state.error}</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Example 2: Multi-Step Form
|
||||
|
||||
```javascript
|
||||
'use server';
|
||||
|
||||
export async function multiStepAction(previousState, formData) {
|
||||
const step = previousState?.step || 1;
|
||||
|
||||
if (step === 1) {
|
||||
const name = formData.get('name');
|
||||
if (!name || name.length < 2) {
|
||||
return { step: 1, error: 'Name is required' };
|
||||
}
|
||||
return { step: 2, data: { name } };
|
||||
}
|
||||
|
||||
if (step === 2) {
|
||||
const email = formData.get('email');
|
||||
if (!email?.includes('@')) {
|
||||
return { step: 2, error: 'Valid email required', data: previousState.data };
|
||||
}
|
||||
|
||||
await db.users.create({
|
||||
data: { ...previousState.data, email },
|
||||
});
|
||||
|
||||
return { step: 3, success: true };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
'use client';
|
||||
|
||||
import { useActionState } from 'react';
|
||||
import { multiStepAction } from './actions';
|
||||
|
||||
export default function MultiStepForm() {
|
||||
const [state, formAction, isPending] = useActionState(multiStepAction, { step: 1 });
|
||||
|
||||
if (state.success) {
|
||||
return <p>Registration complete!</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
{state.step === 1 && (
|
||||
<>
|
||||
<h2>Step 1: Name</h2>
|
||||
<input name="name" type="text" required />
|
||||
{state.error && <p className="error">{state.error}</p>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{state.step === 2 && (
|
||||
<>
|
||||
<h2>Step 2: Email</h2>
|
||||
<p>Name: {state.data.name}</p>
|
||||
<input name="email" type="email" required />
|
||||
{state.error && <p className="error">{state.error}</p>}
|
||||
</>
|
||||
)}
|
||||
|
||||
<button type="submit" disabled={isPending}>
|
||||
{isPending ? 'Processing...' : state.step === 2 ? 'Complete' : 'Next'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</examples>
|
||||
|
||||
<constraints>
|
||||
**MUST**: First parameter
|
||||
|
||||
is `previousState`, second is `formData`; return serializable values (no functions, symbols); access form values via `formData.get('fieldName')`; mark functions with `'use server'` directive.
|
||||
|
||||
**SHOULD**: Validate inputs server-side; return structured error objects for field errors; disable submit button on `isPending`; show loading indicators; use validation libraries (zod, yup).
|
||||
|
||||
**NEVER**: Trust client-side validation alone; return sensitive data in errors; mutate `previousState` directly; skip error handling for async operations; omit authentication/authorization checks.
|
||||
</constraints>
|
||||
|
||||
<validation>
|
||||
**After Implementation**: Test form submission (valid data → success, invalid → errors, check `isPending`); verify Server Action (receives `previousState` and `formData`, returns serializable objects, handles errors); check security (server validates all inputs, authentication/authorization implemented, no sensitive data exposed); test progressive enhancement (disable JS, form submits to permalink, server handles both cases).
|
||||
</validation>
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Optimistic Updates**: Combine with `useOptimistic` for immediate UI feedback:
|
||||
|
||||
```javascript
|
||||
const [state, formAction] = useActionState(addTodo, null);
|
||||
const [optimisticTodos, addOptimisticTodo] = useOptimistic(todos, (state, newTodo) => [
|
||||
...state,
|
||||
newTodo,
|
||||
]);
|
||||
```
|
||||
|
||||
See `../optimistic-updates/SKILL.md`.
|
||||
|
||||
**Reset Form on Success**:
|
||||
|
||||
```javascript
|
||||
const formRef = useRef();
|
||||
|
||||
const [state, formAction] = useActionState(async (prev, formData) => {
|
||||
const result = await submitForm(prev, formData);
|
||||
if (result.success) {
|
||||
formRef.current?.reset();
|
||||
}
|
||||
return result;
|
||||
}, null);
|
||||
|
||||
return (
|
||||
<form ref={formRef} action={formAction}>
|
||||
...
|
||||
</form>
|
||||
);
|
||||
```
|
||||
|
||||
For comprehensive documentation: `research/react-19-comprehensive.md` lines 135-180.
|
||||
````
|
||||
78
skills/using-context-api/SKILL.md
Normal file
78
skills/using-context-api/SKILL.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: using-context-api
|
||||
description: Teaches Context API patterns in React 19 including use() hook for conditional context access. Use when implementing Context, avoiding prop drilling, or managing global state.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Context API Patterns in React 19
|
||||
|
||||
## Basic Pattern
|
||||
|
||||
```javascript
|
||||
import { createContext, use, useState } from 'react';
|
||||
|
||||
const UserContext = createContext(null);
|
||||
|
||||
export function UserProvider({ children }) {
|
||||
const [user, setUser] = useState(null);
|
||||
|
||||
return (
|
||||
<UserContext value={{ user, setUser }}>
|
||||
{children}
|
||||
</UserContext>
|
||||
);
|
||||
}
|
||||
|
||||
export function useUser() {
|
||||
const context = use(UserContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useUser must be used within UserProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
```
|
||||
|
||||
## React 19: Conditional Context Access
|
||||
|
||||
`use()` allows context access inside conditionals (unlike `useContext`):
|
||||
|
||||
```javascript
|
||||
function Component({ isPremium }) {
|
||||
let theme;
|
||||
|
||||
if (isPremium) {
|
||||
theme = use(ThemeContext);
|
||||
}
|
||||
|
||||
return <div className={theme}>Content</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## Splitting Contexts
|
||||
|
||||
Avoid unnecessary re-renders by splitting contexts:
|
||||
|
||||
```javascript
|
||||
const UserContext = createContext(null);
|
||||
const ThemeContext = createContext('light');
|
||||
|
||||
function App() {
|
||||
const [user, setUser] = useState(null);
|
||||
const [theme, setTheme] = useState('light');
|
||||
|
||||
return (
|
||||
<UserContext value={{ user, setUser }}>
|
||||
<ThemeContext value={{ theme, setTheme }}>
|
||||
<Layout />
|
||||
</ThemeContext>
|
||||
</UserContext>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Now theme changes don't re-render components only using UserContext.
|
||||
|
||||
For comprehensive Context patterns, see: `research/react-19-comprehensive.md` lines 288-303, 1326-1342, 1644-1670.
|
||||
106
skills/using-reducers/SKILL.md
Normal file
106
skills/using-reducers/SKILL.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
name: using-reducers
|
||||
description: Teaches useReducer for complex state logic in React 19. Use when state updates depend on previous state, multiple related state values, or complex update logic.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# useReducer Patterns
|
||||
|
||||
## When to Use useReducer
|
||||
|
||||
**Use `useReducer` when:**
|
||||
- Multiple related state values
|
||||
- Complex state update logic
|
||||
- Next state depends on previous state
|
||||
- State transitions follow patterns
|
||||
|
||||
**Use `useState` when:**
|
||||
- Simple independent values
|
||||
- Basic toggle or counter logic
|
||||
- No complex interdependencies
|
||||
|
||||
## Basic Pattern
|
||||
|
||||
```javascript
|
||||
import { useReducer } from 'react';
|
||||
|
||||
const initialState = { count: 0, step: 1 };
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'increment':
|
||||
return { ...state, count: state.count + state.step };
|
||||
case 'decrement':
|
||||
return { ...state, count: state.count - state.step };
|
||||
case 'setStep':
|
||||
return { ...state, step: action.payload };
|
||||
case 'reset':
|
||||
return initialState;
|
||||
default:
|
||||
throw new Error(`Unknown action: ${action.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
function Counter() {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>Count: {state.count}</p>
|
||||
<p>Step: {state.step}</p>
|
||||
|
||||
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
|
||||
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
value={state.step}
|
||||
onChange={(e) => dispatch({ type: 'setStep', payload: +e.target.value })}
|
||||
/>
|
||||
|
||||
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Complex Example: Todo List
|
||||
|
||||
```javascript
|
||||
const initialState = {
|
||||
todos: [],
|
||||
filter: 'all',
|
||||
};
|
||||
|
||||
function todosReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'added':
|
||||
return {
|
||||
...state,
|
||||
todos: [...state.todos, { id: Date.now(), text: action.text, done: false }],
|
||||
};
|
||||
case 'toggled':
|
||||
return {
|
||||
...state,
|
||||
todos: state.todos.map(t =>
|
||||
t.id === action.id ? { ...t, done: !t.done } : t
|
||||
),
|
||||
};
|
||||
case 'deleted':
|
||||
return {
|
||||
...state,
|
||||
todos: state.todos.filter(t => t.id !== action.id),
|
||||
};
|
||||
case 'filterChanged':
|
||||
return {
|
||||
...state,
|
||||
filter: action.filter,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unknown action: ${action.type}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For comprehensive useReducer patterns, see: `research/react-19-comprehensive.md` lines 506-521.
|
||||
118
skills/using-the-use-hook/SKILL.md
Normal file
118
skills/using-the-use-hook/SKILL.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
name: using-the-use-hook
|
||||
description: React 19's use() API for reading Promises/Context conditionally. For async data fetching, Suspense, conditional context access, useContext migration.
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Using the `use()` Hook in React 19
|
||||
|
||||
Teaches when and how to use React 19's new `use()` API: reads Promises (suspending component) and Context (conditionally—inside if statements, loops, after early returns).
|
||||
|
||||
**Activates when**: User mentions `use()`, `use hook`, async patterns, Suspense + data fetching, conditional context, or useContext migration.
|
||||
|
||||
## Key Capabilities vs. Traditional Hooks
|
||||
|
||||
| Feature | Hooks | `use()` |
|
||||
| ------------------ | --------- | -------------- |
|
||||
| Conditional calls | ❌ | ✅ |
|
||||
| After early return | ❌ | ✅ |
|
||||
| Inside loops | ❌ | ✅ |
|
||||
| Read Promises | ❌ | ✅ (suspends) |
|
||||
| Error handling | try-catch | Error Boundary |
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
**Reading Promises**:
|
||||
|
||||
```javascript
|
||||
import { use, Suspense } from 'react';
|
||||
|
||||
async function fetchUser(id) {
|
||||
const res = await fetch(`/api/users/${id}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function UserProfile({ userId }) {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<UserData promise={fetchUser(userId)} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function UserData({ promise }) {
|
||||
const user = use(promise);
|
||||
return <div>{user.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
**Conditional Context Access**:
|
||||
|
||||
```javascript
|
||||
import { use, createContext } from 'react';
|
||||
|
||||
const ThemeContext = createContext('light');
|
||||
|
||||
function Button({ isPrimary, children }) {
|
||||
let className = 'button';
|
||||
if (isPrimary) {
|
||||
const theme = use(ThemeContext);
|
||||
className += ` ${theme}`;
|
||||
}
|
||||
return <button className={className}>{children}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
**Reusable Custom Hook**:
|
||||
|
||||
```javascript
|
||||
import { use, cache } from 'react';
|
||||
|
||||
const getUser = cache(async (id) => {
|
||||
const res = await fetch(`/api/users/${id}`);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
function useUser(userId) {
|
||||
if (!userId) return null;
|
||||
return use(getUser(userId));
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Workflow
|
||||
|
||||
**Async data**: Pass Promise as prop from parent → wrap component in Suspense boundary → use `use(promise)` in child → catch errors with Error Boundary.
|
||||
|
||||
**Conditional context**: Replace `useContext(Context)` with `use(Context)` for flexibility across conditional branches, loops, post-early-return.
|
||||
|
||||
**Migration from useContext**: Find `useContext(Context)` calls → replace with `use(Context)` → benefits: now conditional if needed.
|
||||
|
||||
## Requirements & Validation
|
||||
|
||||
**MUST**:
|
||||
|
||||
- Wrap `use(promise)` in Suspense boundary with fallback
|
||||
- Use Error Boundary (not try-catch) for error handling
|
||||
- Create Promises in parents/Server Components, pass as props
|
||||
- Use `cache()` wrapper to prevent duplicate requests
|
||||
|
||||
**NEVER**:
|
||||
|
||||
- Call `use()` inside try-catch blocks
|
||||
- Create new Promises inside component (causes infinite loops)
|
||||
- Use `use()` outside Component/Hook functions
|
||||
- Omit Suspense boundary for Promises
|
||||
|
||||
**Verify**:
|
||||
|
||||
1. Suspense boundary wraps every `use(promise)` with fallback
|
||||
2. Error Boundary catches Promise errors
|
||||
3. Promises created externally or with `cache()`—stable across renders
|
||||
4. All conditional logic branches tested
|
||||
|
||||
---
|
||||
|
||||
For comprehensive React 19 `use()` documentation:
|
||||
|
||||
`research/react-19-comprehensive.md` lines 241–311 (use API, Promise Patterns, useContext sections).
|
||||
147
skills/validating-forms/SKILL.md
Normal file
147
skills/validating-forms/SKILL.md
Normal file
@@ -0,0 +1,147 @@
|
||||
---
|
||||
name: validating-forms
|
||||
description: Teaches client and server-side form validation patterns in React 19 with Server Actions. Use when implementing form validation, input checking, or error handling.
|
||||
allowed-tools: Read, Write, Edit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Form Validation Patterns
|
||||
|
||||
React 19 forms require validation on both client and server.
|
||||
|
||||
## Client-Side Validation
|
||||
|
||||
Use HTML5 validation + custom logic:
|
||||
|
||||
```javascript
|
||||
<form action={submitForm}>
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
|
||||
/>
|
||||
|
||||
<input
|
||||
name="age"
|
||||
type="number"
|
||||
min="18"
|
||||
max="120"
|
||||
required
|
||||
/>
|
||||
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
## Server-Side Validation (Required)
|
||||
|
||||
**Always validate on server** - client validation can be bypassed:
|
||||
|
||||
```javascript
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('Invalid email'),
|
||||
age: z.number().min(18, 'Must be 18+').max(120),
|
||||
password: z.string().min(8, 'Password must be 8+ characters'),
|
||||
});
|
||||
|
||||
export async function registerUser(previousState, formData) {
|
||||
const data = {
|
||||
email: formData.get('email'),
|
||||
age: Number(formData.get('age')),
|
||||
password: formData.get('password'),
|
||||
};
|
||||
|
||||
const result = schema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
errors: result.error.flatten().fieldErrors,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await db.users.create({ data: result.data });
|
||||
return { success: true, userId: user.id };
|
||||
} catch (error) {
|
||||
if (error.code === 'P2002') {
|
||||
return { errors: { email: ['Email already exists'] } };
|
||||
}
|
||||
return { error: 'Registration failed' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Display Errors
|
||||
|
||||
```javascript
|
||||
'use client';
|
||||
|
||||
import { useActionState } from 'react';
|
||||
import { registerUser } from './actions';
|
||||
|
||||
export default function RegisterForm() {
|
||||
const [state, formAction, isPending] = useActionState(registerUser, null);
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<div>
|
||||
<input name="email" type="email" required />
|
||||
{state?.errors?.email && (
|
||||
<span className="error">{state.errors.email[0]}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input name="age" type="number" required />
|
||||
{state?.errors?.age && (
|
||||
<span className="error">{state.errors.age[0]}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input name="password" type="password" required />
|
||||
{state?.errors?.password && (
|
||||
<span className="error">{state.errors.password[0]}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={isPending}>
|
||||
{isPending ? 'Registering...' : 'Register'}
|
||||
</button>
|
||||
|
||||
{state?.error && <p className="error">{state.error}</p>}
|
||||
{state?.success && <p>Registration successful!</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Libraries
|
||||
|
||||
Recommended: **Zod** for type-safe validation:
|
||||
|
||||
```javascript
|
||||
import { z } from 'zod';
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string(),
|
||||
}).refine(data => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
```
|
||||
|
||||
For comprehensive validation patterns, see: `research/react-19-comprehensive.md`.
|
||||
|
||||
## Related Skills
|
||||
|
||||
**Zod v4 Validation:**
|
||||
- handling-zod-errors skill from the zod-4 plugin - Advanced error customization with unified error API, custom messages, and error formatting for better user feedback
|
||||
- writing-zod-transformations skill from the zod-4 plugin - Built-in string transformations (trim, toLowerCase) for normalizing user input before validation
|
||||
Reference in New Issue
Block a user