react-patterns

📁 benshapyro/cadre-devkit-claude 📅 14 days ago
1
总安装量
1
周安装量
#53928
全站排名
安装命令
npx skills add https://github.com/benshapyro/cadre-devkit-claude --skill react-patterns

Agent 安装分布

cursor 1

Skill 文档

React Patterns Skill

Modern React patterns for TypeScript applications.

Component Structure

File Organization

components/
├── ui/                    # Reusable primitives (Button, Input, Card)
├── features/              # Feature-specific components
│   └── auth/
│       ├── LoginForm.tsx
│       └── SignupForm.tsx
├── layouts/               # Page layouts
└── providers/             # Context providers

Component Template

interface ComponentNameProps {
  // Required props first
  title: string;
  onAction: () => void;
  // Optional props with defaults
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
  children?: React.ReactNode;
}

export function ComponentName({
  title,
  onAction,
  variant = 'primary',
  disabled = false,
  children,
}: ComponentNameProps) {
  return (/* JSX */);
}

Hooks Best Practices

useState

// Prefer explicit types for complex state
const [user, setUser] = useState<User | null>(null);

// Use functional updates when depending on previous state
setCount(prev => prev + 1);

// Group related state or use useReducer for complex state
const [form, setForm] = useState({ name: '', email: '' });

useEffect

// Always specify dependencies explicitly
useEffect(() => {
  fetchData();
}, [userId]); // Only re-run when userId changes

// Cleanup subscriptions
useEffect(() => {
  const subscription = subscribe();
  return () => subscription.unsubscribe();
}, []);

// Avoid objects/arrays in deps - extract primitives
const { id } = user;
useEffect(() => { /* ... */ }, [id]); // Not [user]

useMemo / useCallback

// Memoize expensive calculations
const sortedItems = useMemo(
  () => items.sort((a, b) => a.name.localeCompare(b.name)),
  [items]
);

// Memoize callbacks passed to children
const handleClick = useCallback(() => {
  onAction(id);
}, [onAction, id]);

Custom Hooks

// Extract reusable logic into custom hooks
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// Prefix with "use", return typed values
function useAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  // ... logic

  return { user, loading, signIn, signOut } as const;
}

State Management

Context + useReducer (Complex Local State)

// Define action types and state
type State = { count: number; loading: boolean };
type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'setLoading'; payload: boolean };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + 1 };
    case 'decrement': return { ...state, count: state.count - 1 };
    case 'setLoading': return { ...state, loading: action.payload };
  }
}

// Context provider
const CounterContext = createContext<{
  state: State;
  dispatch: React.Dispatch<Action>;
} | null>(null);

function CounterProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(reducer, { count: 0, loading: false });
  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
}

// Custom hook for consuming
function useCounter() {
  const context = useContext(CounterContext);
  if (!context) throw new Error('useCounter must be used within CounterProvider');
  return context;
}

Server State (React Query / TanStack Query)

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// Fetching data
function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(res => res.json()),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

// Mutations with optimistic updates
function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (user: User) =>
      fetch(`/api/users/${user.id}`, {
        method: 'PUT',
        body: JSON.stringify(user),
      }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

// Usage
function UserList() {
  const { data: users, isLoading, error } = useUsers();
  const updateUser = useUpdateUser();

  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;

  return (/* render users */);
}

When to Use What

Scenario Solution
Simple component state useState
Complex state with many actions useReducer
State shared across components Context + useReducer
Server data (fetch, cache, sync) React Query / SWR
Global app state (auth, theme) Context or Zustand

Component Patterns

Composition over Props

// Prefer composition
<Card>
  <Card.Header>Title</Card.Header>
  <Card.Body>Content</Card.Body>
</Card>

// Over prop drilling
<Card header="Title" body="Content" />

Render Props / Children as Function

interface DataFetcherProps<T> {
  url: string;
  children: (data: T, loading: boolean) => React.ReactNode;
}

function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
  const { data, loading } = useFetch<T>(url);
  return <>{children(data, loading)}</>;
}

Controlled vs Uncontrolled

// Controlled - parent owns state
<Input value={value} onChange={setValue} />

// Uncontrolled - component owns state, use ref to access
<Input defaultValue="initial" ref={inputRef} />

Error Handling

Error Boundaries

class ErrorBoundary extends Component<Props, State> {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: ErrorInfo) {
    console.error('Error caught:', error, info);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? <div>Something went wrong</div>;
    }
    return this.props.children;
  }
}

Async Error Handling

const [error, setError] = useState<Error | null>(null);

async function handleSubmit() {
  try {
    setError(null);
    await submitForm(data);
  } catch (e) {
    setError(e instanceof Error ? e : new Error('Unknown error'));
  }
}

Performance

Avoid Unnecessary Renders

  • Use React.memo() for pure components receiving complex props
  • Split context providers to minimize re-renders
  • Use useMemo for expensive derived state
  • Lazy load heavy components with React.lazy()

Code Splitting

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  );
}

TypeScript Integration

Event Handlers

function handleChange(e: React.ChangeEvent<HTMLInputElement>) { }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { }
function handleClick(e: React.MouseEvent<HTMLButtonElement>) { }

Generic Components

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

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

Anti-Patterns

  • Don’t mutate state directly
  • Don’t call hooks conditionally or in loops
  • Don’t use array index as key for dynamic lists
  • Don’t fetch data in useEffect without cleanup/cancellation
  • Don’t ignore dependency array warnings
  • Don’t overuse context for frequently-changing values

Version

  • v1.0.0 (2025-12-05): Added YAML frontmatter, initial documented version