| name | react-patterns |
| skill_category | code |
| description | React component patterns, hooks, anti-patterns, and quality rules — with deterministic regex-based React anti-pattern detection for JSX, TSX, useState, useEffect, conditional hooks, key-prop anti-patterns, and context usage. Use proactively when reviewing React component files (.jsx, .tsx), enforcing hooks rules (no conditional hooks), catching key-prop anti-patterns in list rendering, or running a pre-review scan of React codebases. Run the checker for deterministic scanning. |
| version | 2.0.0 |
| source | converted from .claude/skills/code/react-patterns.md (2026-05-20); L3 checker added |
| when_to_use | ["Reviewing React component files (.jsx, .tsx)","Enforcing hooks rules (no conditional hooks)","Catching key-prop anti-patterns in list rendering","Pre-review scan of React codebases"] |
| allowed-tools | ["Read","Grep","Glob","Bash"] |
| tags | ["react","hooks","components","jsx","frontend","ui","patterns"] |
| related_skills | ["javascript-patterns","testing-patterns"] |
| trigger_files | ["*.tsx","*.jsx","**/components/**","**/hooks/**"] |
| trigger_keywords | ["react","hooks","useState","useEffect","component","jsx","props","context"] |
React Patterns
Modern React patterns, hooks best practices, anti-patterns, and quality rules.
Core Principles
| Principle | Description |
|---|
| Composition | Small, focused components over inheritance |
| Unidirectional | Data flows down, events flow up |
| Declarative | Describe what, not how |
| Hooks | Functional components with hooks over class components |
Component Patterns
Functional Components
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
export function Button({
label,
onClick,
variant = 'primary',
disabled = false,
}: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
}
class Button extends React.Component { }
Composition Over Props Drilling
function Card({ children }: { children: React.ReactNode }) {
return <div className="card">{children}</div>;
}
Card.Header = function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="card-header">{children}</div>;
};
Card.Body = function CardBody({ children }: { children: React.ReactNode }) {
return <div className="card-body">{children}</div>;
};
<Card>
<Card.Header>Title</Card.Header>
<Card.Body>Content</Card.Body>
</Card>
Render Props / Children as Function
interface DataFetcherProps<T> {
url: string;
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode;
}
function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
const { data, loading, error } = useFetch<T>(url);
return <>{children(data, loading, error)}</>;
}
<DataFetcher<User[]> url="/api/users">
{(users, loading, error) => {
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <UserList users={users!} />;
}}
</DataFetcher>
Hooks Best Practices
useState
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<Item[]>([]);
setCount(prev => prev + 1);
setItems(prev => [...prev, newItem]);
setUser({ ...user, name: 'New' });
user.name = 'New';
setUser(user);
useEffect
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
const response = await fetch(url, { signal: controller.signal });
setData(await response.json());
} catch (error) {
if (!controller.signal.aborted) {
setError(error as Error);
}
}
}
fetchData();
return () => controller.abort();
}, [url]);
useEffect(() => {
fetchData(userId);
}, []);
useEffect(() => {
doSomething(options);
}, [options]);
useMemo / useCallback
const sortedItems = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
const handleClick = useCallback(
(id: string) => {
onSelect(id);
},
[onSelect]
);
const double = useMemo(() => value * 2, [value]);
const handleClick = useCallback(() => {
onSelect(selectedId);
}, []);
Custom Hooks
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
const [theme, setTheme] = useLocalStorage('theme', 'light');
Anti-Patterns to Avoid
Index as Key
{items.map((item, index) => (
<Item key={index} data={item} />
))}
{items.map(item => (
<Item key={item.id} data={item} />
))}
Props in State
function UserProfile({ user }: { user: User }) {
const [userData, setUserData] = useState(user);
}
function UserProfile({ user }: { user: User }) {
const displayName = user.name.toUpperCase();
}
useEffect for Transforms
const [items, setItems] = useState([]);
const [filtered, setFiltered] = useState([]);
useEffect(() => {
setFiltered(items.filter(i => i.active));
}, [items]);
const filtered = useMemo(
() => items.filter(i => i.active),
[items]
);
Excessive Re-renders
<Child
config={{ theme: 'dark' }}
onClick={() => handleClick(id)}
/>
const config = useMemo(() => ({ theme: 'dark' }), []);
const handleChildClick = useCallback(() => handleClick(id), [id]);
<Child config={config} onClick={handleChildClick} />
State Management
Context for Global State
interface AuthContextType {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
Reducer for Complex State
type Action =
| { type: 'SET_LOADING' }
| { type: 'SET_DATA'; payload: Data }
| { type: 'SET_ERROR'; payload: Error };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_LOADING':
return { ...state, loading: true, error: null };
case 'SET_DATA':
return { ...state, loading: false, data: action.payload };
case 'SET_ERROR':
return { ...state, loading: false, error: action.payload };
}
}
Quality Checklist
| Check | Rule |
|---|
| Unique keys | Never use index as key for dynamic lists |
| Hook deps | All dependencies listed, no lint suppressions |
| No props in state | Derive from props directly |
| Stable callbacks | useCallback for event handlers passed to children |
| Error boundaries | Wrap feature sections |
| Typed props | Interface for all component props |
| Controlled inputs | value + onChange, not defaultValue |
| Cleanup effects | Return cleanup function for subscriptions |
| Memoize expensive | useMemo for costly computations only |
| Custom hooks | Extract reusable stateful logic |
Invocation — React Anti-Pattern Checker (L3 Script)
Run the checker on any JSX or TSX file. Consume its output only — the script source never enters context.
Scope note: This is a regex-based structural checker, not a full JSX/AST parser. It reliably catches three closed-set patterns:
key={index} (index used as key in map)
- Missing
key prop in .map( JSX render (JSX element without key= on same or following lines)
- Hooks called inside conditional blocks (
if/&&/ternary body)
It does NOT detect prop-drilling depth (requires component tree analysis), missing useEffect dependencies (requires full scope analysis), or render performance issues. Use eslint-plugin-react-hooks for those.
Run via Bash (file argument):
python .claude/skills/code/react-patterns/scripts/react_patterns.py path/to/Component.tsx
Run via Bash (stdin — paste code or pipe):
cat path/to/Component.jsx | python .claude/skills/code/react-patterns/scripts/react_patterns.py -
The script outputs:
- A JSON object with
findings (list of anti-patterns with rule, severity, line, message) and a summary of counts by severity.
- A human-readable markdown table sorted by severity descending.
Detected rules:
INDEX_AS_KEY HIGH — key={index} or key={i} in map callback (causes incorrect reconciliation on reorder)
MISSING_KEY MEDIUM — .map( call with JSX element that has no key= prop within 5 lines
HOOK_IN_CONDITIONAL HIGH — Hook call (use...()) inside an if statement or && expression (violates Rules of Hooks)
Error handling: Script exits 1 on unreadable file. Exits 0 even if findings are present.
What the agent does with the output:
- HIGH findings (INDEX_AS_KEY, HOOK_IN_CONDITIONAL) must be fixed before merge.
- MISSING_KEY findings may be false positives for static lists — verify that the list is truly static before dismissing.