Agent skill
react-patterns
React development patterns. Use when building React components, managing state, creating custom hooks, or optimizing React applications. Covers React 19 features, TypeScript integration, and composition patterns.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/react-patterns-dralgorhythm-claude-agentic-frame
SKILL.md
React Patterns
Platform: Web and Mobile (shared React patterns). For React Native-specific patterns (Pressable, ScrollView, FlashList, safe areas), see the react-native-patterns skill.
Overview
Patterns for building maintainable React applications with TypeScript, leveraging React 19 features and composition patterns.
Workflows
- Choose appropriate component composition pattern
- Apply TypeScript types for props and events
- Implement custom hooks for shared logic
- Add performance optimizations where needed
- Handle loading and error states with Suspense/boundaries
- Validate component render behavior
Feedback Loops
- Components render without TypeScript errors
- Props are properly typed and validated
- Custom hooks have clear return types
- No unnecessary re-renders (use React DevTools Profiler)
- Error boundaries catch component errors
- Loading states work with Suspense
Reference Implementation
1. Component Composition
Compound Components
// Shares implicit state between parent and children
interface TabsContextValue {
activeTab: string;
setActiveTab: (id: string) => void;
}
const TabsContext = createContext<TabsContextValue | null>(null);
function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
{children}
</TabsContext.Provider>
);
}
function TabList({ children }: { children: ReactNode }) {
return <div role="tablist">{children}</div>;
}
function Tab({ id, children }: { id: string; children: ReactNode }) {
const ctx = use(TabsContext);
if (!ctx) throw new Error('Tab must be used within Tabs');
const { activeTab, setActiveTab } = ctx;
return (
<button
role="tab"
aria-selected={activeTab === id}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
}
// Usage
<Tabs defaultTab="profile">
<TabList>
<Tab id="profile">Profile</Tab>
<Tab id="settings">Settings</Tab>
</TabList>
</Tabs>
Render Props
// Share logic while giving consumer render control
interface MousePosition {
x: number;
y: number;
}
function MouseTracker({ render }: { render: (pos: MousePosition) => ReactNode }) {
const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return <>{render(position)}</>;
}
// Usage
<MouseTracker render={({ x, y }) => <p>Mouse at {x}, {y}</p>} />
Slot Pattern
// Named slots for flexible composition
interface CardProps {
header?: ReactNode;
footer?: ReactNode;
children: ReactNode;
}
function Card({ header, footer, children }: CardProps) {
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// Usage
<Card
header={<h2>Title</h2>}
footer={<button>Action</button>}
>
Content here
</Card>
2. React 19 Features
use() Hook
// Unwrap promises and context
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // Suspends until resolved
return <div>{user.name}</div>;
}
// Context without useContext
function ThemedButton() {
const theme = use(ThemeContext); // Simpler than useContext
return <button className={theme}>Click</button>;
}
Actions and useActionState
// Server actions with pending states
async function updateUser(prevState: { error?: string }, formData: FormData) {
'use server';
const name = formData.get('name') as string;
// Validate and update...
return { error: undefined };
}
function UserForm() {
const [state, formAction, isPending] = useActionState(updateUser, {});
return (
<form action={formAction}>
<input name="name" disabled={isPending} />
{state.error && <p className="error">{state.error}</p>}
<button disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
</form>
);
}
useOptimistic
// Optimistic UI updates
function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo]
);
async function handleAdd(formData: FormData) {
const todo = { id: crypto.randomUUID(), text: formData.get('text') as string };
addOptimisticTodo(todo);
await saveTodo(todo);
}
return (
<form action={handleAdd}>
{optimisticTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}
<input name="text" />
<button>Add</button>
</form>
);
}
3. Custom Hooks
Object Return Pattern (multiple values)
// Return object for named access
function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const login = async (credentials: Credentials) => {
const user = await api.login(credentials);
setUser(user);
};
return { user, loading, login, logout };
}
// Usage
const { user, login } = useAuth();
Tuple Return Pattern (2-3 values)
// Return tuple for positional access (like useState)
function useToggle(initial = false): [boolean, () => void] {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
return [value, toggle];
}
// Usage
const [isOpen, toggleOpen] = useToggle();
Composing Hooks
// Build complex hooks from simple ones
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
function useDarkMode() {
const [isDark, setIsDark] = useLocalStorage('darkMode', false);
useEffect(() => {
document.body.classList.toggle('dark', isDark);
}, [isDark]);
return [isDark, setIsDark] as const;
}
4. TypeScript + React
Props Typing
// Use interface for extensibility
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
loading?: boolean;
}
function Button({ variant = 'primary', loading, children, ...props }: ButtonProps) {
return (
<button className={variant} disabled={loading} {...props}>
{loading ? 'Loading...' : children}
</button>
);
}
Generic Components
// Type-safe data components
interface ListProps<T> {
items: T[];
renderItem: (item: T) => 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>
);
}
// Usage with full type inference
<List
items={users}
renderItem={user => <span>{user.name}</span>}
keyExtractor={user => user.id}
/>
Refs as Props (React 19+)
// React 19 simplifies ref forwarding - refs are regular props
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
ref?: React.Ref<HTMLInputElement>;
}
function Input({ label, ref, ...props }: InputProps) {
return (
<label>
{label}
<input ref={ref} {...props} />
</label>
);
}
// Usage - ref works like any other prop
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return <Input label="Name" ref={inputRef} />;
}
Note: forwardRef is deprecated in React 19. Use ref as a regular prop instead.
Event Handlers
// Properly typed event handlers
function Form() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
// Process formData...
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
return <form onSubmit={handleSubmit}><input onChange={handleChange} /></form>;
}
5. State Management
useReducer for Complex State
// Better than multiple useState for related state
interface State {
data: User[];
loading: boolean;
error: string | null;
}
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: User[] }
| { type: 'FETCH_ERROR'; payload: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, data: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
}
}
function UserList() {
const [state, dispatch] = useReducer(reducer, {
data: [],
loading: false,
error: null,
});
useEffect(() => {
dispatch({ type: 'FETCH_START' });
fetchUsers()
.then(data => dispatch({ type: 'FETCH_SUCCESS', payload: data }))
.catch(err => dispatch({ type: 'FETCH_ERROR', payload: err.message }));
}, []);
return <>{/* render state */}</>;
}
6. Performance Patterns
React.memo
// Prevent re-renders when props haven't changed
interface ItemProps {
item: Item;
onDelete: (id: string) => void;
}
const ListItem = memo(function ListItem({ item, onDelete }: ItemProps) {
console.log('Rendering', item.id);
return (
<li>
{item.name}
<button onClick={() => onDelete(item.id)}>Delete</button>
</li>
);
});
// Parent component
function List() {
const [items, setItems] = useState<Item[]>([]);
const handleDelete = useCallback((id: string) => {
setItems(items => items.filter(item => item.id !== id));
}, []);
return (
<>
{items.map(item => (
<ListItem key={item.id} item={item} onDelete={handleDelete} />
))}
</>
);
}
useMemo and useCallback
// useMemo for expensive computations
function DataTable({ data }: { data: Row[] }) {
const sortedData = useMemo(() => {
console.log('Sorting...');
return [...data].sort((a, b) => a.name.localeCompare(b.name));
}, [data]);
return <>{/* render sortedData */}</>;
}
// useCallback for stable function references
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // Stable reference
return <MemoizedChild onClick={handleClick} />;
}
Code Splitting
// 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>
);
}
7. Error Handling
Error Boundary
// Catch rendering errors
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <div>Something went wrong</div>;
}
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<ErrorMessage />}>
<App />
</ErrorBoundary>
Suspense for Loading
// Handle async data loading
function UserProfile({ userId }: { userId: string }) {
const user = use(fetchUser(userId)); // Suspends
return <div>{user.name}</div>;
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserProfile userId="123" />
</Suspense>
);
}
Best Practices
- Composition over inheritance - Use composition patterns for flexibility
- Type everything - Leverage TypeScript for compile-time safety
- Colocate state - Keep state as close to where it's used as possible
- Extract custom hooks - Share logic across components with hooks
- Name hooks with use prefix - Follow React naming conventions
- Stable references - Use useCallback/useMemo to prevent unnecessary re-renders
- Error boundaries - Wrap component trees to catch rendering errors
- Suspense for loading - Use Suspense instead of manual loading states
- Server boundaries - Mark client-only components with 'use client' directive
- Avoid prop drilling - Use context or composition for deeply nested props
Anti-Patterns
- Using forwardRef in React 19 - Use ref as a regular prop instead
- Class components for new code - Use function components and hooks
- Mutating state directly - Always use setState or reducer actions
- Missing dependency arrays - Include all dependencies in useEffect/useMemo/useCallback
- Overusing useMemo/useCallback - Only optimize when necessary (profile first)
- Context for everything - Use context sparingly; prefer props or state management library
- Derived state in useState - Compute derived values during render instead
- useEffect for derived state - Use useMemo or compute directly in render
- Index as key - Use stable unique IDs for list keys
- Spreading {...props} blindly - Be explicit about which props are passed down
- Ignoring TypeScript errors - Never use 'any' or '// @ts-ignore' as shortcuts
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?