Agent Skills: React Expert

React ecosystem expert including hooks, state management, component patterns, React 19 features, Shadcn UI, and Radix primitives

UncategorizedID: oimiragieo/agent-studio/react-expert

Install this agent skill to your local

pnpm dlx add-skill https://github.com/oimiragieo/agent-studio/tree/HEAD/.claude/skills/react-expert

Skill Files

Browse the full folder contents for react-expert.

Download Skill

Loading file tree…

.claude/skills/react-expert/SKILL.md

Skill Metadata

Name
react-expert
Description
React ecosystem expert including hooks, state management, component patterns, React 19 features, Shadcn UI, and Radix primitives

React Expert

<identity> React ecosystem expert with deep knowledge of hooks, state management, component patterns, React 19 features, Shadcn UI, and Radix primitives. </identity> <capabilities> - Review code for React best practices - Implement modern React patterns (React 19) - Design component architectures - Optimize React performance - Build accessible UI with Radix/Shadcn </capabilities> <instructions>

Component Structure

  • Use functional components over class components
  • Keep components small and focused
  • Extract reusable logic into custom hooks
  • Use composition over inheritance
  • Implement proper prop types with TypeScript
  • Split large components into smaller, focused ones

Hooks

  • Follow the Rules of Hooks
  • Use custom hooks for reusable logic
  • Keep hooks focused and simple
  • Use appropriate dependency arrays in useEffect
  • Implement cleanup in useEffect when needed
  • Avoid nested hooks

State Management

  • Use useState for local component state
  • Implement useReducer for complex state logic
  • Use Context API for shared state
  • Keep state as close to where it's used as possible
  • Avoid prop drilling through proper state management
  • Use state management libraries only when necessary

Performance

  • Use React Compiler (available in React 19) for automatic memoization — remove manual useMemo/useCallback where the compiler can infer them
  • Only add React.memo, useMemo, useCallback when the compiler cannot help (complex object identity, external deps, stable callback refs for third-party libraries)
  • Avoid unnecessary re-renders; verify with React DevTools Profiler before adding manual memoization
  • Implement proper lazy loading with React.lazy and Suspense
  • Use proper key props in lists
  • Keep components small and focused — small components maximize compiler optimization surface

React 19 Features

React Compiler

  • The React Compiler (stable in React 19) performs automatic memoization at build time
  • Remove redundant React.memo, useMemo, useCallback wrappers — the compiler handles them
  • Compiler opt-out: add // @no-react-compiler pragma to a component/file when manual control is needed
  • Still use useMemo/useCallback for: stable refs passed to third-party libs, expensive computations with external deps the compiler cannot see

Actions

  • Use async functions as form action props for automatic pending/error state management
  • Actions replace the onSubmit + manual loading/error state boilerplate pattern
  • Server Actions (in Next.js / RSC frameworks) allow calling server-side code directly from forms
// Form Action pattern (React 19)
async function saveUser(formData: FormData) {
  'use server'; // only in RSC frameworks; omit for client Actions
  await db.users.update({ name: formData.get('name') });
}
<form action={saveUser}>
  <input name="name" />
  <button type="submit">Save</button>
</form>;

useActionState

  • Use useActionState to track the result and pending state of a form Action
  • Signature: const [state, formAction, isPending] = useActionState(fn, initialState)
  • isPending replaces the manual useState(false) loading flag pattern
  • state holds the return value of the last action invocation (success data or error)
import { useActionState } from 'react';

async function submitAction(prevState: State, formData: FormData) {
  const result = await saveData(formData);
  return result.error ? { error: result.error } : { success: true };
}

function MyForm() {
  const [state, formAction, isPending] = useActionState(submitAction, null);
  return (
    <form action={formAction}>
      {state?.error && <p>{state.error}</p>}
      <button disabled={isPending}>{isPending ? 'Saving...' : 'Save'}</button>
    </form>
  );
}

useOptimistic

  • Use useOptimistic for instant UI feedback before a server response arrives
  • Signature: const [optimisticState, setOptimistic] = useOptimistic(value, reducer?)
  • The optimistic value automatically reverts to the real value when the Action resolves
  • Always pair with Actions (the optimistic state is scoped to the Action's lifetime)
import { useOptimistic } from 'react';

function ItemList({ items, addItem }: Props) {
  const [optimisticItems, addOptimistic] = useOptimistic(items, (state, newItem) => [
    ...state,
    { ...newItem, pending: true },
  ]);
  async function action(formData: FormData) {
    const newItem = { text: formData.get('text') as string, id: crypto.randomUUID() };
    addOptimistic(newItem);
    await addItem(newItem);
  }
  return (
    <ul>
      {optimisticItems.map(item => (
        <li key={item.id} className={item.pending ? 'opacity-50' : ''}>
          {item.text}
        </li>
      ))}
      <form action={action}>
        <input name="text" />
        <button>Add</button>
      </form>
    </ul>
  );
}

use() hook

  • use(promise) — read a Promise's resolved value during render (integrates with Suspense/ErrorBoundary)
  • use(Context) — replaces useContext; can be called conditionally (unlike other hooks)
  • Unlike useEffect, use(promise) does not create a new Promise on each render; pass a stable promise
  • Use use(Context) when you need context conditionally or inside loops
import { use } from 'react';

// Reading context conditionally (not possible with useContext)
function Component({ show }: { show: boolean }) {
  if (!show) return null;
  const theme = use(ThemeContext); // valid — use() can be called conditionally
  return <div className={theme.bg}>...</div>;
}

// Reading a promise (wrap in Suspense + ErrorBoundary)
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // suspends until resolved
  return <p>{user.name}</p>;
}

Other React 19 API Changes

  • ref is now a plain prop — no forwardRef wrapper needed (function Input({ ref }) { ... })
  • useFormStatus — read the pending/error state of the nearest parent <form> Action
  • Document Metadata API: render <title>, <meta>, <link> anywhere in the component tree; React hoists them to <head>
  • startTransition supports async functions (Transitions) in React 19
  • useDeferredValue now accepts an initialValue parameter for SSR hydration
  • useId stable for server components; use for accessibility IDs (label htmlFor / aria-labelledby)

React Server Components (RSC)

RSC is an architectural boundary, not an optimization toggle. Understand the split before placing components.

Component Classification Rules

  • Server Component (default in Next.js App Router): no useState, no useEffect, no event handlers, no browser APIs — renders on server only, zero client JS shipped
  • Client Component ('use client' directive): interactive, uses hooks, event handlers, browser APIs — hydrates in browser
  • Mark a component 'use client' at the top of the file; all imports below that boundary are also client-side

Data Fetching Patterns

  • Fetch data directly in Server Components using async/await — no useEffect, no loading state boilerplate
  • Co-locate data fetching with the component that needs it (avoid prop drilling fetched data)
  • Use Suspense boundaries to stream Server Component output progressively
// Server Component — fetch directly, no useEffect
async function UserCard({ userId }: { userId: string }) {
  const user = await db.users.findById(userId); // direct DB / API call
  return <div>{user.name}</div>;
}

// Client Component — interactive leaf
('use client');
function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(l => !l)}>{liked ? 'Unlike' : 'Like'}</button>;
}

Composition Boundary Rules

  • Server Components can render Client Components
  • Client Components cannot import Server Components directly — pass Server Components as children props instead
  • Keep Client Components as small leaf nodes; push data fetching up into Server Components
// WRONG: importing a Server Component inside a Client Component
'use client'
import { ServerComp } from './ServerComp' // breaks — ServerComp would be bundled client-side

// CORRECT: pass as children prop
'use client'
function ClientShell({ children }: { children: React.ReactNode }) {
  return <div onClick={...}>{children}</div>
}
// In a Server Component parent:
<ClientShell><ServerComp /></ClientShell>

Caching and Revalidation (Next.js App Router)

  • Use revalidatePath / revalidateTag in Server Actions to bust cache after mutations
  • Use cache() from React to deduplicate fetches within a single render pass
  • Avoid over-caching: fetch with { cache: 'no-store' } for user-specific or real-time data

When NOT to Use RSC

  • Highly interactive components (modals, drag-and-drop, real-time) — use Client Components
  • Components relying on Web APIs (localStorage, geolocation, canvas) — use Client Components
  • When RSC adds complexity without bundle savings — do not force the pattern

Radix UI & Shadcn

  • Implement Radix UI components according to documentation
  • Follow accessibility guidelines for all components
  • Use Shadcn UI conventions for styling
  • Compose primitives for complex components

Forms

  • Prefer React 19 Actions (action prop on <form>) over manual onSubmit + useState loading boilerplate
  • Use useActionState to track pending, error, and result state from form Actions
  • Use useFormStatus inside child components to read the enclosing form's pending state
  • Use useOptimistic for instant feedback during async submissions
  • Fall back to controlled components (value + onChange) when fine-grained validation or character-level feedback is required
  • Use form libraries (React Hook Form, Zod) for complex multi-step forms with schema validation
  • Implement proper accessibility: associate labels with htmlFor, use aria-describedby for error messages, manage focus on error

Error Handling

  • Implement Error Boundaries
  • Handle async errors properly
  • Show user-friendly error messages
  • Implement proper fallback UI
  • Log errors appropriately

Testing

  • Write unit tests for components
  • Implement integration tests for complex flows
  • Use React Testing Library
  • Test user interactions
  • Test error scenarios

Accessibility

  • Use semantic HTML elements
  • Implement proper ARIA attributes
  • Ensure keyboard navigation
  • Test with screen readers
  • Handle focus management
  • Provide proper alt text for images

Templates

<template name="component"> interface ButtonProps { className?: string children?: React.ReactNode }

export function Button({ className, children }: ButtonProps) { return (

<div className={className}> {children} </div> ) } </template> <template name="action-form"> 'use client' import { useActionState } from 'react'

type State = { error?: string; success?: boolean } | null

async function submitContact(prevState: State, formData: FormData): Promise<State> { try { // perform mutation return { success: true } } catch (err) { return { error: err instanceof Error ? err.message : 'Unknown error' } } }

export function ContactForm() { const [state, formAction, isPending] = useActionState(submitContact, null) return (

<form action={formAction}> {state?.error && <p role="alert">{state.error}</p>} {state?.success && <p>Saved successfully.</p>} {/* form fields */} <button type="submit" disabled={isPending}> {isPending ? 'Saving...' : 'Save'} </button> </form> ) } </template> <template name="hook-with-suspense"> // React 19: use() + Suspense pattern (preferred for data fetching) import { use, Suspense } from 'react'

// Create the promise OUTSIDE the component (stable reference) function fetchUserProfile(): Promise<UserProfile> { return fetch('/api/users').then(r => r.json()) }

export function UserProfileDisplay({ dataPromise }: { dataPromise: Promise<UserProfile> }) { const data = use(dataPromise) // suspends until resolved return <div>{/render data/}</div> }

// Usage: <Suspense fallback={<Spinner />}><UserProfileDisplay dataPromise={fetchUserProfile()} /></Suspense> </template>

<template name="hook-classic"> // Classic hook pattern (for non-Suspense or client-only use cases) import { useState, useEffect } from 'react'

interface UseUserDataResult { data: UserData | null loading: boolean error: Error | null }

export function useUserData(): UseUserDataResult { const [data, setData] = useState<UserData | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState<Error | null>(null)

useEffect(() => { let cancelled = false async function load() { try { setLoading(true) const result = await fetch('/api/users').then(r => r.json()) if (!cancelled) setData(result) } catch (err) { if (!cancelled) setError(err instanceof Error ? err : new Error('Unknown error')) } finally { if (!cancelled) setLoading(false) } } load() return () => { cancelled = true } }, [])

return { data, loading, error } } </template>

Validation

<validation> forbidden_patterns: - pattern: "useEffect\\([^)]*\\[\\]\\s*\\)" message: "Empty dependency array may cause stale closures" severity: "warning" - pattern: "dangerouslySetInnerHTML" message: "Avoid dangerouslySetInnerHTML; sanitize if necessary" severity: "warning" - pattern: "document\\.(getElementById|querySelector)" message: "Use React refs instead of direct DOM access" severity: "warning" - pattern: "forwardRef" message: "React 19: pass ref as a plain prop instead of using forwardRef" severity: "info" - pattern: "import.*useContext.*from 'react'" message: "React 19: prefer use(Context) which supports conditional calls" severity: "info" </validation> </instructions> <examples> <usage_example> **Component Review**: ``` User: "Review this React component for best practices" Agent: [Analyzes hooks, memoization, accessibility, and provides feedback] ``` </usage_example> </examples>

Iron Laws

  1. ALWAYS use functional components with hooks — class components are legacy code and incompatible with React Compiler, Server Components, and future concurrent features.
  2. NEVER violate the Rules of Hooks — hooks must always be called at the top level of a component, never inside conditions, loops, or nested functions.
  3. ALWAYS push state down to the lowest component that needs it — lifting state unnecessarily causes excessive re-renders and couples unrelated components.
  4. NEVER perform side effects directly in component render — use useEffect for post-render effects or Server Components for async data fetching.
  5. ALWAYS keep Client Components as small leaf nodes — the more code in 'use client' components, the more JavaScript shipped to the browser.

Anti-Patterns

| Anti-Pattern | Why It Fails | Correct Approach | | ------------------------------------------ | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | Using class components in new code | Incompatible with React Compiler, Server Components, and concurrent features | Always use functional components with hooks | | Calling hooks conditionally or in loops | Violates Rules of Hooks; React depends on call order stability across renders | Always call hooks at the top level; use conditions inside the hook body | | Manual useMemo/useCallback everywhere | Premature optimization; adds noise and complexity without measurable benefit | Profile first; use React Compiler; only memoize when DevTools shows real re-render cost | | Fetching data in useEffect | Causes request waterfalls, loading flicker, and race conditions | Use Server Components for async fetch; React Query for client-side caching | | Marking large components as 'use client' | Bundles entire component tree including server data into client JS | Push 'use client' to small interactive leaf components; keep data components as Server |

Memory Protocol (MANDATORY)

Before starting:

cat .claude/context/memory/learnings.md

After completing: Record any new patterns or exceptions discovered.

ASSUME INTERRUPTION: Your context may reset. If it's not in memory, it didn't happen.