commit ef94998b7ecb9edfc0e4c2269f88879308ac98f3 Author: Zhongwei Li Date: Sat Nov 29 18:22:28 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..69a75bb --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,15 @@ +{ + "name": "react-19", + "description": "React 19 patterns, hooks, and best practices for modern React applications", + "version": "1.0.0", + "author": { + "name": "Daniel Jankowski", + "email": "djank0113@gmail.com" + }, + "skills": [ + "./skills" + ], + "hooks": [ + "./hooks" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..046e343 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# react-19 + +React 19 patterns, hooks, and best practices for modern React applications diff --git a/hooks/hooks.json b/hooks/hooks.json new file mode 100644 index 0000000..deb4b19 --- /dev/null +++ b/hooks/hooks.json @@ -0,0 +1,27 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate-react-19.sh", + "timeout": 5000 + } + ] + } + ], + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/check-react-version.sh", + "timeout": 3000 + } + ] + } + ] + } +} diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..1086d10 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,149 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:djankies/claude-configs:react-19", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "0f9911012ada0dbdf0f3c136026eb719d0de1af8", + "treeHash": "0a1aefdf5242fde375ccbf3a147a3129e978b021d4f2a0a99a8eae24f5bb2b96", + "generatedAt": "2025-11-28T10:16:29.159902Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "react-19", + "description": "React 19 patterns, hooks, and best practices for modern React applications", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "df71ed4b55622f5ceb43cb107910dcb33818a69e652350753c18869bd25cd542" + }, + { + "path": "hooks/hooks.json", + "sha256": "faa3a2dcf2bf199eafc5ee71caf53c9ee553566c20c6f625f71daf4e9246b4b7" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "23e15cf5a72824750b06645c1ee6ea09c1641d33786fc29c519ff24da400c3eb" + }, + { + "path": "skills/reviewing-state-management/SKILL.md", + "sha256": "4104e0790de7c6e3f76aa475b68cd32615815ea1208a9efb5f8f05cb239094fe" + }, + { + "path": "skills/optimizing-with-react-compiler/SKILL.md", + "sha256": "f4088d046de311eee6774535cdad520bcedb922a6b7d4bf9d744c443277b539f" + }, + { + "path": "skills/following-the-rules-of-hooks/SKILL.md", + "sha256": "2e354aaebd3703794af7206422a62a885ca8d7cd07ccaf30fd7f491c997639ba" + }, + { + "path": "skills/reviewing-performance-patterns/SKILL.md", + "sha256": "aa8df8a3577bdc3cf4b3c997f1df68d44632880cf916a8a24791b634b68ccd57" + }, + { + "path": "skills/using-the-use-hook/SKILL.md", + "sha256": "323fbb558dcf58b6e534c8930864f58293caf151a3823ff0ec22fdbda62449cc" + }, + { + "path": "skills/implementing-optimistic-updates/SKILL.md", + "sha256": "adff7e87fc87329834970dfc12548069e921609bfd4dad72a642376db3d60d22" + }, + { + "path": "skills/using-context-api/SKILL.md", + "sha256": "f294fbe6bdc93a93a777fd4c689a8aa5b4896cd75016eb899c151d40a59f50f7" + }, + { + "path": "skills/managing-server-vs-client-boundaries/SKILL.md", + "sha256": "925b3a50bfff5e6e82149afc15c74968ef904a95ab7818ff2ee00204601fe1bc" + }, + { + "path": "skills/implementing-code-splitting/SKILL.md", + "sha256": "9933826f3b4f19391d88fbd6cc87ca61a762caced5aa9170e0b45e2b34b01b9a" + }, + { + "path": "skills/testing-server-actions/SKILL.md", + "sha256": "114e50cadfdda34d2752f336bddebd8feb7024058050ec014671fed370e89c93" + }, + { + "path": "skills/testing-hooks/SKILL.md", + "sha256": "ea39a6600f1e23662c6d801207253bc4faba5e222def4b9f4a3930004eeeb944" + }, + { + "path": "skills/composing-components/SKILL.md", + "sha256": "c8870a62d2f588c1561bef1576fcd59f5fa36bdb8317e93ad78f54e4fdf846e7" + }, + { + "path": "skills/migrating-from-forwardref/SKILL.md", + "sha256": "1496eed3a49f77c2aec0b3bd6edc67e15139179120e36176b4eccab927443407" + }, + { + "path": "skills/supporting-custom-elements/SKILL.md", + "sha256": "08f8460b82a017135856abb6d35d8b869e36ae45ba16d2debd10996d2edea075" + }, + { + "path": "skills/managing-local-vs-global-state/SKILL.md", + "sha256": "13a14089e72b47ec160058ab2bd7387379777f12459bfdecff779d93d423fde4" + }, + { + "path": "skills/using-action-state/SKILL.md", + "sha256": "f7f01bae194a47a55b66d8612b7cb645003b775e67191575a1c260cb9c2641a4" + }, + { + "path": "skills/reviewing-hook-patterns/SKILL.md", + "sha256": "dae987bdb76bb6bb32ff59f715abcbaccd70e16862860060d24c2226a2eee423" + }, + { + "path": "skills/implementing-server-actions/SKILL.md", + "sha256": "ab0aa393bafe7124c0d2703bc569ded3fa77b2e4a45c45bb0bcd4616c0034120" + }, + { + "path": "skills/reviewing-server-actions/SKILL.md", + "sha256": "9ff59c39163ca90babd80f603203fb5879520c21ea43a88544907b70a8c0215c" + }, + { + "path": "skills/reviewing-test-quality/SKILL.md", + "sha256": "be583211bad937deab1c949d59df88512e5a0c5a311036a9bca15450ad15916c" + }, + { + "path": "skills/reviewing-component-architecture/SKILL.md", + "sha256": "fdf759be60d0e957e87ba8f554d0b6be27238ea4bfe2ab7f6ecbddab6b897ed0" + }, + { + "path": "skills/using-reducers/SKILL.md", + "sha256": "9fba6838677ebdcb0eaf059b6fc82310f087aba743b30592835d396f46b6586b" + }, + { + "path": "skills/tracking-form-status/SKILL.md", + "sha256": "c5212e307e44aaa4c659adf50581fcfbafe427c1d458f2dbc0e4d995b5d8d0d3" + }, + { + "path": "skills/preloading-resources/SKILL.md", + "sha256": "92e4d10c77ba83a5f7a309a9704dc62eee8a0a5fc0b0c7f058dfb2115a6716ea" + }, + { + "path": "skills/testing-components/SKILL.md", + "sha256": "1ae707e7e86ab3f195d0706dead78f4ee02d990f5336b81c9362a05690342131" + }, + { + "path": "skills/validating-forms/SKILL.md", + "sha256": "8afac6c4ae95fe3b854e656072e7435940d87f7319702d93a4e9f7c20ae3621a" + } + ], + "dirSha256": "0a1aefdf5242fde375ccbf3a147a3129e978b021d4f2a0a99a8eae24f5bb2b96" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/composing-components/SKILL.md b/skills/composing-components/SKILL.md new file mode 100644 index 0000000..7e7300a --- /dev/null +++ b/skills/composing-components/SKILL.md @@ -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 + + +This skill teaches you how to compose components effectively using React 19 patterns. + + + +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 + + + +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 + + + +## Pattern 1: Children Prop + +```javascript +function Card({ children, header, footer }) { + return ( +
+ {header &&
{header}
} +
{children}
+ {footer &&
{footer}
} +
+ ); +} + +Title} footer={}> +

Card content goes here

+
; +``` + +## 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 ( + +
{children}
+
+ ); +} + +export function TabList({ children }) { + return
{children}
; +} + +export function Tab({ id, children }) { + const { activeTab, setActiveTab } = use(TabsContext); + + return ( + + ); +} + +export function TabPanel({ id, children }) { + const { activeTab } = use(TabsContext); + + if (activeTab !== id) return null; + + return
{children}
; +} +``` + +Usage: + +```javascript + + + Profile + Settings + + + + + + + + + + +``` + +## 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 }); +} + + (loading ? : )} +/>; +``` + +
+ + +## Example: Modal with Composition + +```javascript +function Modal({ children, isOpen, onClose }) { + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()}> + {children} +
+
+ ); +} + +function ModalHeader({ children }) { + return
{children}
; +} + +function ModalBody({ children }) { + return
{children}
; +} + +function ModalFooter({ children }) { + return
{children}
; +} + +Modal.Header = ModalHeader; +Modal.Body = ModalBody; +Modal.Footer = ModalFooter; +``` + +Usage: + +```javascript + setShowModal(false)}> + +

Confirm Action

+
+ + +

Are you sure you want to proceed?

+
+ + + + + +
+``` + +For comprehensive composition patterns, see: `research/react-19-comprehensive.md` lines 1263-1293. +
+ + +## 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.) + diff --git a/skills/following-the-rules-of-hooks/SKILL.md b/skills/following-the-rules-of-hooks/SKILL.md new file mode 100644 index 0000000..4670515 --- /dev/null +++ b/skills/following-the-rules-of-hooks/SKILL.md @@ -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
{count}
; +} + +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
{user.name}
; +} +``` +**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 ; + }); +} +``` +✅ **Right:** +```javascript +function List({ items }) { + const [selected, setSelected] = useState({}); + return items.map(item => ( + 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 ; +} +``` +✅ **Right:** +```javascript +function Form() { + const [loading, setLoading] = useState(false); + function handleSubmit() { + setLoading(true); + } + return ; +} +``` +**Pattern:** Hook at component level, setter in handler. +### Hooks in Classes +❌ **Wrong:** +```javascript +function BadCounter() { + const [count, setCount] = useState(0); + return
{count}
; +} +``` +✅ **Right:** +```javascript +function Counter() { + const [count, setCount] = useState(0); + return
{count}
; +} +``` +**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
{user.name}
; +} +``` +✅ **Right:** +```javascript +function User({ userId }) { + const user = useUser(userId || null); + if (!userId) return null; + return
{user.name}
; +} +``` +**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 diff --git a/skills/implementing-code-splitting/SKILL.md b/skills/implementing-code-splitting/SKILL.md new file mode 100644 index 0000000..9d7bc58 --- /dev/null +++ b/skills/implementing-code-splitting/SKILL.md @@ -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 ( + Loading...}> + + + ); +} +``` + +## 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 ( + + }> + + } /> + } /> + } /> + + + + ); +} +``` + +## 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 ( + <> + + + + }> + {view === 'chart' ? : } + + + ); +} +``` + +## 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. diff --git a/skills/implementing-optimistic-updates/SKILL.md b/skills/implementing-optimistic-updates/SKILL.md new file mode 100644 index 0000000..22671d6 --- /dev/null +++ b/skills/implementing-optimistic-updates/SKILL.md @@ -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 + + +This skill teaches you how to use React 19's `useOptimistic` hook for immediate UI feedback during async operations. + + + +- 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 + + + +`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 + + + +## 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 ( +
    + {optimisticMessages.map((msg) => ( +
  • + {msg.text} {msg.sending && (Sending...)} +
  • + ))} +
+ ); +} +``` +
+ + +## 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 ( + + ); +} +``` + +For comprehensive useOptimistic documentation, see: `research/react-19-comprehensive.md` lines 182-240. + + + +## 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 + + + +## 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. + diff --git a/skills/implementing-server-actions/SKILL.md b/skills/implementing-server-actions/SKILL.md new file mode 100644 index 0000000..62e3f50 --- /dev/null +++ b/skills/implementing-server-actions/SKILL.md @@ -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 ( + + + + + + ); +} +``` + +## 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 ...; +} +``` + +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 diff --git a/skills/managing-local-vs-global-state/SKILL.md b/skills/managing-local-vs-global-state/SKILL.md new file mode 100644 index 0000000..264c050 --- /dev/null +++ b/skills/managing-local-vs-global-state/SKILL.md @@ -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 ; +} +``` + +**Lifted State:** +```javascript +function Parent() { + const [filter, setFilter] = useState('all'); + + return ( + <> + + + + ); +} +``` + +**Context (React 19 with `use()`):** +```javascript +import { createContext, use } from 'react'; + +const ThemeContext = createContext('light'); + +function App() { + const [theme, setTheme] = useState('light'); + + return ( + + + + ); +} + +function DeepComponent() { + const { theme, setTheme } = use(ThemeContext); + + return ; +} +``` + +## 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 ( + + + + ); +} +``` + +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
Mouse: {position.x}, {position.y}
; +} +``` + +For comprehensive state management patterns, see: `research/react-19-comprehensive.md` lines 1293-1342. diff --git a/skills/managing-server-vs-client-boundaries/SKILL.md b/skills/managing-server-vs-client-boundaries/SKILL.md new file mode 100644 index 0000000..de64b0c --- /dev/null +++ b/skills/managing-server-vs-client-boundaries/SKILL.md @@ -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 ; +} +``` + +**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 ( +
+

{data.title}

+ +
+ ); +} + +// ❌ WRONG: Client Component importing Server Component +('use client'); +import ServerComponent from './ServerComponent'; // ERROR + +// ✅ RIGHT: Pass Server Component as children + + +; +``` + +## 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 ( + <> + + + + ); +} +``` + +**Server data in Client Component**: Pass as props (serialize data) + +```javascript +async function ServerComponent() { + const data = await fetchData(); + return ; +} +``` + +**Server logic from Client**: Use Server Actions + +```javascript +async function ServerComponent() { + async function serverAction() { + 'use server'; + await db.update(...); + } + return ; +} +``` + +## 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 ( +
+ {product.name} +
+

{product.name}

+

{product.description}

+

${product.price}

+ +
+ +
+ ); +} + +// 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 ( + + ); +} + +// Server Component with real-time Client +async function Dashboard() { + const stats = await db.stats.getLatest(); + return ( + <> + + + + ); +} + +// 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 ?
Active Users: {metrics.activeUsers}
:
Connecting...
; +} + +// 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 ( +
+ +