# React Expert

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

- Skill: `oimiragieo/react-expert` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add oimiragieo/react-expert`
- Raw SKILL.md: https://api.skillmd.com/api/skills/oimiragieo/react-expert/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: oimiragieo (https://skillmd.com/u/oimiragieo)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/oimiragieo/react-expert

---


# 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

```tsx
// 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)

```tsx
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)

```tsx
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

```tsx
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

```tsx
// 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

```tsx
// 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:**

```bash
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.

