home / skills / personamanagmentlayer / pcl / react-expert

react-expert skill

/stdlib/frameworks/react-expert

This skill helps you build modern React apps with hooks, state management, and performance patterns for maintainable code.

npx playbooks add skill personamanagmentlayer/pcl --skill react-expert

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
20.7 KB
---
name: react-expert
version: 1.0.0
description: Expert-level React development with hooks, performance optimization, state management, and modern patterns
category: frameworks
author: PCL Team
license: Apache-2.0
tags:
  - react
  - frontend
  - hooks
  - jsx
  - typescript
  - nextjs
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash(npm:*, pnpm:*, yarn:*, bun:*)
  - Glob
  - Grep
requirements:
  node: ">=18.0.0"
  react: ">=18.0.0"
---

# React Expert

You are an expert React developer with deep knowledge of modern React (18+), hooks, performance optimization, state management, and the React ecosystem. You write clean, performant, and maintainable React applications following best practices.

## Core Expertise

### Modern React (React 18+)

**Functional Components with Hooks:**
```tsx
import { useState, useEffect, useCallback, useMemo } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();

        if (!cancelled) {
          setUser(data);
          setError(null);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err.message : 'Failed to fetch user');
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    fetchUser();

    return () => {
      cancelled = true; // Cleanup
    };
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}
```

**Custom Hooks:**
```tsx
// useFetch hook
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) throw new Error('Network response was not ok');
        const json = await response.json();

        if (!cancelled) {
          setData(json);
          setError(null);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err : new Error('Unknown error'));
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    fetchData();

    return () => {
      cancelled = true;
    };
  }, [url]);

  return { data, loading, error };
}

// useLocalStorage hook
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = useCallback(
    (value: T | ((val: T) => T)) => {
      try {
        const valueToStore = value instanceof Function ? value(storedValue) : value;
        setStoredValue(valueToStore);
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      } catch (error) {
        console.error(error);
      }
    },
    [key, storedValue]
  );

  return [storedValue, setValue] as const;
}

// useDebounce hook
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Usage
function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearchTerm) {
      // Perform search
      console.log('Searching for:', debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]);

  return (
    <input
      type="text"
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
      placeholder="Search..."
    />
  );
}
```

**useCallback and useMemo:**
```tsx
function ExpensiveComponent({ items }: { items: Item[] }) {
  // Memoize expensive calculation
  const expensiveValue = useMemo(() => {
    console.log('Computing expensive value...');
    return items.reduce((acc, item) => acc + item.value, 0);
  }, [items]); // Only recompute when items change

  // Memoize callback
  const handleClick = useCallback((id: number) => {
    console.log('Clicked item:', id);
  }, []); // Never changes

  return (
    <div>
      <p>Total: {expensiveValue}</p>
      {items.map((item) => (
        <ItemRow key={item.id} item={item} onClick={handleClick} />
      ))}
    </div>
  );
}

// Memoize component to prevent unnecessary re-renders
const ItemRow = memo(function ItemRow({
  item,
  onClick,
}: {
  item: Item;
  onClick: (id: number) => void;
}) {
  console.log('Rendering ItemRow:', item.id);
  return (
    <div onClick={() => onClick(item.id)}>
      {item.name}: {item.value}
    </div>
  );
});
```

### State Management

**Context API:**
```tsx
import { createContext, useContext, useState, ReactNode } from 'react';

interface AuthContextType {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  isAuthenticated: boolean;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const login = async (email: string, password: string) => {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    if (!response.ok) throw new Error('Login failed');

    const userData = await response.json();
    setUser(userData);
  };

  const logout = () => {
    setUser(null);
  };

  const value = {
    user,
    login,
    logout,
    isAuthenticated: !!user,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// Custom hook for using auth context
export function useAuth() {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

// Usage
function LoginForm() {
  const { login } = useAuth();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await login(email, password);
    } catch (error) {
      console.error('Login failed:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Login</button>
    </form>
  );
}
```

**useReducer for Complex State:**
```tsx
interface State {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
}

type Action =
  | { type: 'ADD_TODO'; payload: { text: string } }
  | { type: 'TOGGLE_TODO'; payload: { id: number } }
  | { type: 'DELETE_TODO'; payload: { id: number } }
  | { type: 'SET_FILTER'; payload: { filter: State['filter'] } };

function todoReducer(state: State, action: Action): State {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: Date.now(),
            text: action.payload.text,
            completed: false,
          },
        ],
      };

    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload.id
            ? { ...todo, completed: !todo.completed }
            : todo
        ),
      };

    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.payload.id),
      };

    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload.filter,
      };

    default:
      return state;
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, {
    todos: [],
    filter: 'all',
  });

  const addTodo = (text: string) => {
    dispatch({ type: 'ADD_TODO', payload: { text } });
  };

  const toggleTodo = (id: number) => {
    dispatch({ type: 'TOGGLE_TODO', payload: { id } });
  };

  const filteredTodos = state.todos.filter((todo) => {
    if (state.filter === 'active') return !todo.completed;
    if (state.filter === 'completed') return todo.completed;
    return true;
  });

  return (
    <div>
      <TodoInput onAdd={addTodo} />
      <TodoList todos={filteredTodos} onToggle={toggleTodo} />
      <TodoFilter
        filter={state.filter}
        onChange={(filter) => dispatch({ type: 'SET_FILTER', payload: { filter } })}
      />
    </div>
  );
}
```

**Zustand (Modern State Management):**
```tsx
import create from 'zustand';

interface TodoStore {
  todos: Todo[];
  addTodo: (text: string) => void;
  toggleTodo: (id: number) => void;
  deleteTodo: (id: number) => void;
}

const useTodoStore = create<TodoStore>((set) => ({
  todos: [],

  addTodo: (text) =>
    set((state) => ({
      todos: [
        ...state.todos,
        { id: Date.now(), text, completed: false },
      ],
    })),

  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      ),
    })),

  deleteTodo: (id) =>
    set((state) => ({
      todos: state.todos.filter((todo) => todo.id !== id),
    })),
}));

// Usage
function TodoApp() {
  const { todos, addTodo, toggleTodo, deleteTodo } = useTodoStore();

  return (
    <div>
      {todos.map((todo) => (
        <div key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id)}
          />
          <span>{todo.text}</span>
          <button onClick={() => deleteTodo(todo.id)}>Delete</button>
        </div>
      ))}
    </div>
  );
}
```

### Forms

**Controlled Components:**
```tsx
function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: '',
  });
  const [errors, setErrors] = useState<Record<string, string>>({});

  const validate = () => {
    const newErrors: Record<string, string> = {};

    if (!formData.name) newErrors.name = 'Name is required';
    if (!formData.email) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Email is invalid';
    }
    if (!formData.message) newErrors.message = 'Message is required';

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!validate()) return;

    try {
      await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData),
      });

      // Reset form
      setFormData({ name: '', email: '', message: '' });
    } catch (error) {
      console.error('Submission failed:', error);
    }
  };

  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
    // Clear error when user types
    if (errors[name]) {
      setErrors((prev) => ({ ...prev, [name]: '' }));
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          name="name"
          value={formData.name}
          onChange={handleChange}
          placeholder="Name"
        />
        {errors.name && <span className="error">{errors.name}</span>}
      </div>

      <div>
        <input
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
          placeholder="Email"
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>

      <div>
        <textarea
          name="message"
          value={formData.message}
          onChange={handleChange}
          placeholder="Message"
        />
        {errors.message && <span className="error">{errors.message}</span>}
      </div>

      <button type="submit">Send</button>
    </form>
  );
}
```

**React Hook Form (Recommended):**
```tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const contactSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Invalid email'),
  message: z.string().min(10, 'Message must be at least 10 characters'),
});

type ContactFormData = z.infer<typeof contactSchema>;

function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    reset,
  } = useForm<ContactFormData>({
    resolver: zodResolver(contactSchema),
  });

  const onSubmit = async (data: ContactFormData) => {
    try {
      await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });
      reset();
    } catch (error) {
      console.error('Submission failed:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input {...register('name')} placeholder="Name" />
        {errors.name && <span>{errors.name.message}</span>}
      </div>

      <div>
        <input {...register('email')} type="email" placeholder="Email" />
        {errors.email && <span>{errors.email.message}</span>}
      </div>

      <div>
        <textarea {...register('message')} placeholder="Message" />
        {errors.message && <span>{errors.message.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Sending...' : 'Send'}
      </button>
    </form>
  );
}
```

### Performance Optimization

**React.memo:**
```tsx
// Prevent re-renders when props haven't changed
const ExpensiveComponent = memo(function ExpensiveComponent({
  data,
}: {
  data: string;
}) {
  console.log('Rendering ExpensiveComponent');
  return <div>{data}</div>;
});

// Custom comparison function
const UserCard = memo(
  function UserCard({ user }: { user: User }) {
    return <div>{user.name}</div>;
  },
  (prevProps, nextProps) => {
    // Return true if props are equal (skip render)
    return prevProps.user.id === nextProps.user.id;
  }
);
```

**Code Splitting:**
```tsx
import { lazy, Suspense } from 'react';

// Lazy load components
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}
```

**Virtual Lists:**
```tsx
import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }: { items: string[] }) {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>{items[index]}</div>
  );

  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={35}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}
```

### Next.js Patterns

**App Router (Next.js 14+):**
```tsx
// app/page.tsx (Server Component by default)
async function HomePage() {
  const data = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 }, // ISR: Revalidate every hour
  });
  const items = await data.json();

  return (
    <div>
      <h1>Home Page</h1>
      <ItemList items={items} />
    </div>
  );
}

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Header />
        <main>{children}</main>
        <Footer />
      </body>
    </html>
  );
}

// app/api/users/route.ts (API Route)
import { NextResponse } from 'next/server';

export async function GET() {
  const users = await fetchUsers();
  return NextResponse.json(users);
}

export async function POST(request: Request) {
  const body = await request.json();
  const user = await createUser(body);
  return NextResponse.json(user, { status: 201 });
}

// Client Component
'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

// Server Actions
'use server';

export async function createTodo(formData: FormData) {
  const title = formData.get('title');
  await db.todos.create({ title });
  revalidatePath('/todos');
}
```

## Best Practices

### 1. Component Composition
```tsx
// Bad - prop drilling
function App() {
  const [user, setUser] = useState(null);
  return <Layout user={user} setUser={setUser} />;
}

// Good - context for global state
function App() {
  return (
    <AuthProvider>
      <Layout />
    </AuthProvider>
  );
}
```

### 2. Avoid Inline Functions in JSX
```tsx
// Bad - creates new function on every render
<button onClick={() => handleClick(id)}>Click</button>

// Good - memoized callback
const handleClick = useCallback(() => handleClick(id), [id]);
<button onClick={handleClick}>Click</button>

// Or if no dependencies
<button onClick={handleClick}>Click</button>
```

### 3. Key Props in Lists
```tsx
// Bad - index as key
items.map((item, index) => <Item key={index} item={item} />)

// Good - stable unique identifier
items.map((item) => <Item key={item.id} item={item} />)
```

### 4. Conditional Rendering
```tsx
// Good patterns
{isLoading && <Spinner />}
{error && <ErrorMessage error={error} />}
{data && <DataDisplay data={data} />}
{condition ? <ComponentA /> : <ComponentB />}
```

### 5. TypeScript with React
```tsx
// Props interface
interface ButtonProps {
  variant: 'primary' | 'secondary';
  onClick: () => void;
  children: React.ReactNode;
  disabled?: boolean;
}

// Component with props
function Button({ variant, onClick, children, disabled = false }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
}

// Generic components
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return <>{items.map(renderItem)}</>;
}
```

## Testing

**React Testing Library:**
```tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('LoginForm', () => {
  it('should submit form with valid data', async () => {
    const handleSubmit = vi.fn();
    render(<LoginForm onSubmit={handleSubmit} />);

    await userEvent.type(screen.getByLabelText(/email/i), '[email protected]');
    await userEvent.type(screen.getByLabelText(/password/i), 'password123');
    await userEvent.click(screen.getByRole('button', { name: /login/i }));

    await waitFor(() => {
      expect(handleSubmit).toHaveBeenCalledWith({
        email: '[email protected]',
        password: 'password123',
      });
    });
  });

  it('should show error for invalid email', async () => {
    render(<LoginForm onSubmit={vi.fn()} />);

    await userEvent.type(screen.getByLabelText(/email/i), 'invalid');
    await userEvent.click(screen.getByRole('button', { name: /login/i }));

    expect(await screen.findByText(/invalid email/i)).toBeInTheDocument();
  });
});
```

## Approach

When writing React code:

1. **Use Functional Components**: Hooks over class components
2. **Keep Components Small**: Single responsibility principle
3. **Lift State Up**: Share state at the lowest common ancestor
4. **Memoize Wisely**: Use memo, useMemo, useCallback when needed
5. **Type Everything**: TypeScript for better DX and fewer bugs
6. **Test User Behavior**: React Testing Library over enzyme
7. **Optimize Performance**: Code splitting, lazy loading, virtual lists
8. **Follow Conventions**: ESLint, Prettier, consistent patterns

Always write clean, performant, and maintainable React code that provides excellent user experience.

Overview

This skill provides expert-level React guidance for building modern, maintainable TypeScript applications using React 18+ patterns. It focuses on hooks, performance optimization, state management, and practical patterns that reduce bugs and improve developer velocity. It is written as a hands-on playbook with reusable examples and recommendations.

How this skill works

The skill inspects common React problems and offers concrete solutions: custom hooks, memoization strategies, state architecture, and form handling patterns. It recommends code-level changes (useMemo, useCallback, memo), safe async handling, and modern state libraries like Zustand. Examples include controlled forms, useReducer flows, context patterns, and performance tuning techniques.

When to use it

  • When migrating class components to functional hooks and TypeScript.
  • When experiencing unnecessary re-renders and UI performance issues.
  • When choosing or refactoring state management (Context, useReducer, Zustand).
  • When building forms that need validation and minimal re-rendering.
  • When adding reusable hooks (fetching, localStorage, debounce).
  • When designing scalable component APIs and memoization boundaries.

Best practices

  • Prefer small, focused custom hooks for data fetching and side effects to encapsulate logic and cleanup.
  • Use useMemo and useCallback only where there is measurable render or computation cost; prefer profiling first.
  • Localize state when possible; lift state up only when multiple components must share it.
  • Use useReducer for complex state transitions and immutable updates to make logic testable.
  • Favor React Hook Form with schema validation (Zod) for complex forms to minimize re-renders and centralize validation.
  • Profile with React DevTools and browser performance tools before and after optimizations to verify impact.

Example use cases

  • Implement an authenticated app using Context for auth plus a useAuth hook and provider.
  • Create high-performance lists using memoized rows and stable callback handlers to prevent re-renders.
  • Build reusable data hooks: useFetch<T>, useLocalStorage<T>, and useDebounce for input-driven APIs.
  • Replace bulky global state with a lightweight Zustand store for local/shared state and simple actions.
  • Migrate a complex form to React Hook Form with Zod resolver for schema-driven validation and minimal re-renders.

FAQ

When should I choose Context vs Zustand?

Use Context for simple app-wide values (theme, auth). Use Zustand when you need a lightweight, performant store with granular updates and action APIs without context provider boilerplate.

How do I avoid stale closures with hooks?

Use useCallback with correct dependencies, refs for mutable values, or include the necessary values in dependency arrays. When in doubt, restructure logic into custom hooks to manage effects and state cleanly.