react-patterns
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
useMemofor 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
More from benshapyro/cadre-devkit-claude
frontend-design
Create distinctive, memorable user interfaces that avoid generic AI aesthetics. Use when designing UI/UX, planning visual direction, or building pages and layouts.
10error-handler
Provides battle-tested error handling patterns for TypeScript and Python. Use when implementing error handling, creating try/catch blocks, or handling exceptions.
5tailwind-conventions
Consistent Tailwind CSS patterns for React/Next.js applications. Use when styling components with Tailwind, adding responsive design, implementing dark mode, or organizing utility classes.
4product-discovery
Methodology for discovering and specifying new software products. Use when starting greenfield projects, exploring new ideas, or defining MVP scope.
4test-generator
Generates Jest or Pytest tests following Ben's testing standards. Use when creating tests, adding test coverage, writing unit tests, mocking dependencies, or when user mentions testing, test cases, Jest, Pytest, fixtures, assertions, or coverage.
3devkit-knowledge
Knowledge base for the Cadre DevKit. Use when answering questions about the devkit structure, commands, skills, hooks, agents, or workflows.
3