Agent skill
react-component-patterns
React component development patterns for building UI components with hooks, state management, error handling, loading states. Use when creating React components, managing component state, handling user interactions, or working with component lifecycle.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/react-component-patterns
SKILL.md
React Component Patterns
Core Principles
- Single Responsibility - One component does one thing well
- Composition Over Inheritance - Build complex UIs from simple components
- Props Down, Events Up - Data flows down, events bubble up
- Always Handle All States - Loading, error, empty, success
Component Structure Pattern
CORRECT: Well-Structured Component
// client/src/components/ChatInterface.jsx
import { useState, useEffect } from 'react';
import { generatePage } from '../utils/api';
import MessageBubble from './MessageBubble';
export default function ChatInterface({ config, onGenerationComplete }) {
// 1. State declarations (grouped by purpose)
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// 2. Effects
useEffect(() => {
// Initialize or cleanup
return () => {
// Cleanup if needed
};
}, []);
// 3. Event handlers
const handleSubmit = async (e) => {
e.preventDefault();
if (!input.trim()) return;
setLoading(true);
setError(null);
try {
const response = await generatePage(config, input, messages);
setMessages([...messages, { role: 'user', content: input }, response]);
setInput('');
onGenerationComplete(response.html);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
// 4. Early returns for states
if (error) {
return (
<div className="text-red-500 p-4">
<p>Error: {error}</p>
<button onClick={() => setError(null)}>Retry</button>
</div>
);
}
// 5. Main render
return (
<div className="flex flex-col h-full">
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4">
{messages.length === 0 ? (
<div className="text-gray-500 text-center">
Start a conversation...
</div>
) : (
messages.map((msg, idx) => (
<MessageBubble key={idx} message={msg} />
))
)}
</div>
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={loading}
className="w-full px-4 py-2 border rounded"
placeholder="Describe changes..."
/>
<button
type="submit"
disabled={loading || !input.trim()}
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{loading ? 'Generating...' : 'Send'}
</button>
</form>
</div>
);
}
WRONG: Poorly Structured Component
// ❌ DON'T DO THIS
export default function ChatInterface(props) {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
// ❌ No loading or error state
// ❌ No error handling
const handleSubmit = () => {
generatePage(props.config, input, messages).then(response => {
// ❌ Directly mutating state
messages.push({ role: 'user', content: input });
messages.push(response);
setMessages(messages);
});
};
// ❌ No empty state handling
// ❌ No loading feedback
return (
<div>
{messages.map((msg, idx) => <div key={idx}>{msg.content}</div>)}
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={handleSubmit}>Send</button>
</div>
);
}
The Four States Pattern
Every component that fetches data MUST handle these states:
- Loading - Data is being fetched
- Error - Something went wrong
- Empty - No data to display
- Success - Data is available
CORRECT: All States Handled
export default function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUsers()
.then(setUsers)
.catch(setError)
.finally(() => setLoading(false));
}, []);
// Loading state
if (loading) {
return <div className="p-4">Loading users...</div>;
}
// Error state
if (error) {
return (
<div className="p-4 text-red-500">
<p>Error: {error.message}</p>
<button onClick={() => window.location.reload()}>Retry</button>
</div>
);
}
// Empty state
if (users.length === 0) {
return (
<div className="p-4 text-gray-500">
No users found.
</div>
);
}
// Success state
return (
<div className="p-4">
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
Custom Hooks Pattern
Extract reusable logic into custom hooks:
CORRECT: Custom Hook
// client/src/hooks/useChat.js
import { useState, useCallback } from 'react';
import { generatePage } from '../utils/api';
export function useChat(config) {
const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const sendMessage = useCallback(async (userMessage) => {
setLoading(true);
setError(null);
try {
const response = await generatePage(config, userMessage, messages);
setMessages(prev => [
...prev,
{ role: 'user', content: userMessage },
{ role: 'assistant', content: response.message, html: response.html }
]);
return response;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}, [config, messages]);
const clearMessages = useCallback(() => {
setMessages([]);
setError(null);
}, []);
return {
messages,
loading,
error,
sendMessage,
clearMessages
};
}
Using the Custom Hook
export default function ChatInterface({ config }) {
const { messages, loading, error, sendMessage } = useChat(config);
const [input, setInput] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
if (!input.trim()) return;
try {
await sendMessage(input);
setInput('');
} catch (err) {
// Error already handled by hook
}
};
// ... render UI
}
Props Patterns
Props Destructuring
// ✅ CORRECT: Destructure at function signature
export default function MessageBubble({ message, onDelete, className }) {
return (
<div className={className}>
<p>{message.content}</p>
{onDelete && <button onClick={onDelete}>Delete</button>}
</div>
);
}
// ❌ WRONG: Accessing props.property everywhere
export default function MessageBubble(props) {
return (
<div className={props.className}>
<p>{props.message.content}</p>
{props.onDelete && <button onClick={props.onDelete}>Delete</button>}
</div>
);
}
Optional Props with Defaults
export default function Button({
children,
onClick,
variant = 'primary', // Default value
disabled = false,
className = ''
}) {
const baseClasses = 'px-4 py-2 rounded';
const variantClasses = {
primary: 'bg-blue-500 text-white',
secondary: 'bg-gray-200 text-gray-800'
};
return (
<button
onClick={onClick}
disabled={disabled}
className={`${baseClasses} ${variantClasses[variant]} ${className}`}
>
{children}
</button>
);
}
Event Handler Patterns
CORRECT: Proper Event Handling
export default function Form({ onSubmit }) {
const [value, setValue] = useState('');
// Named handler functions
const handleSubmit = (e) => {
e.preventDefault(); // Prevent page reload
onSubmit(value);
setValue(''); // Clear after submit
};
const handleChange = (e) => {
setValue(e.target.value);
};
const handleKeyDown = (e) => {
if (e.key === 'Enter' && e.ctrlKey) {
handleSubmit(e);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
<button type="submit">Submit</button>
</form>
);
}
WRONG: Inline Functions and Missing preventDefault
// ❌ DON'T DO THIS
export default function Form({ onSubmit }) {
const [value, setValue] = useState('');
return (
<form onSubmit={() => onSubmit(value)}> {/* ❌ Missing e.preventDefault() */}
<input
value={value}
onChange={(e) => setValue(e.target.value)} {/* ⚠️ OK for simple cases */}
/>
<button onClick={() => { /* ❌ Use type="submit" instead */
onSubmit(value);
setValue('');
}}>
Submit
</button>
</form>
);
}
Conditional Rendering Patterns
CORRECT: Clear Conditional Rendering
export default function StatusMessage({ status, message }) {
// Early return for no status
if (!status) return null;
// Conditional rendering with ternary (simple)
return (
<div className={status === 'error' ? 'text-red-500' : 'text-green-500'}>
{message}
</div>
);
}
// For complex conditions, use early returns
export default function ComplexComponent({ user, loading, error }) {
if (loading) return <Loading />;
if (error) return <Error error={error} />;
if (!user) return <Login />;
return <Dashboard user={user} />;
}
WRONG: Nested Ternaries
// ❌ DON'T DO THIS: Unreadable nested ternaries
return (
<div>
{loading ? (
<Loading />
) : error ? (
<Error />
) : user ? (
user.isPremium ? (
<PremiumDashboard />
) : (
<FreeDashboard />
)
) : (
<Login />
)}
</div>
);
List Rendering Patterns
CORRECT: Proper Keys and Mapping
export default function MessageList({ messages }) {
// ✅ Use unique IDs as keys
return (
<div>
{messages.map((message) => (
<MessageBubble
key={message.id} // ✅ Unique ID
message={message}
/>
))}
</div>
);
}
// If no ID available, use index only as last resort
export default function StaticList({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li> // ⚠️ OK only if list never changes
))}
</ul>
);
}
WRONG: Index as Key (when list changes)
// ❌ DON'T DO THIS: Index as key with dynamic list
export default function DynamicList({ items, onDelete }) {
return (
<div>
{items.map((item, index) => (
<div key={index}> {/* ❌ Index will cause issues when deleting */}
{item.name}
<button onClick={() => onDelete(index)}>Delete</button>
</div>
))}
</div>
);
}
Checklist
Before Writing a Component
- What is the single responsibility of this component?
- What props does it need?
- What state does it need?
- Does it fetch data? (Handle all 4 states)
- Can logic be extracted into a custom hook?
After Writing a Component
- All states handled (loading, error, empty, success)
- Props are destructured
- Event handlers prevent default when needed
- List items have proper keys
- Early returns for special states
- No inline function definitions in render (unless very simple)
- Component is focused and not doing too much
Integration with Other Skills
- api-client-patterns: How to call APIs from components
- express-api-patterns: Understanding backend endpoints
- testing-patterns: How to test React components
- systematic-debugging: Debugging component issues
Common Mistakes to Avoid
- ❌ Not handling loading/error states
- ❌ Using index as key in dynamic lists
- ❌ Mutating state directly
- ❌ Missing preventDefault in form submissions
- ❌ Accessing undefined properties without checks
- ❌ Not cleaning up effects
- ❌ Too many responsibilities in one component
- ❌ Prop drilling instead of composition or context
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?