Agent skill
react-component-development
Design and implement React components with hooks, composition patterns, and performance optimization. Use when creating React components, implementing custom hooks, or optimizing component performance.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/design/react-component-development
SKILL.md
React Component Development Specialist
Specialized in creating reusable, performant React components using modern patterns and hooks.
When to Use This Skill
- Creating functional React components
- Implementing built-in hooks (useState, useEffect, useContext, etc.)
- Designing custom hooks
- Optimizing component performance
- Implementing component composition patterns
- Building compound components
- Managing component props and types
Core Principles
- Functional Components: Use function components over class components
- Hooks for Logic: Use hooks to encapsulate and reuse stateful logic
- Composition Over Inheritance: Compose components to build complex UIs
- Single Responsibility: Each component has one clear purpose
- Props Typing: Always type props with TypeScript
- Performance Awareness: Use memo, useMemo, and useCallback judiciously
Implementation Guidelines
Basic Component Structure
import { FC } from 'react'
interface UserCardProps {
id: string
name: string
email: string
onDelete?: (id: string) => void
}
// WHY: FC type provides children prop automatically if needed
export const UserCard: FC<UserCardProps> = ({ id, name, email, onDelete }) => {
return (
<div className="user-card">
<h3>{name}</h3>
<p>{email}</p>
{onDelete && (
<button onClick={() => onDelete(id)}>Delete</button>
)}
</div>
)
}
// Alternative: explicit return type
export function UserCard({ id, name, email, onDelete }: UserCardProps): JSX.Element {
return (
<div className="user-card">
<h3>{name}</h3>
<p>{email}</p>
{onDelete && (
<button onClick={() => onDelete(id)}>Delete</button>
)}
</div>
)
}
Props Patterns
// Optional props
interface ButtonProps {
label: string
onClick: () => void
disabled?: boolean
variant?: 'primary' | 'secondary' | 'danger'
}
// Props with children
interface CardProps {
title: string
children: React.ReactNode
}
// Props extending HTML attributes
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string
error?: string
}
export const Input: FC<InputProps> = ({ label, error, ...inputProps }) => {
return (
<div>
<label>{label}</label>
<input {...inputProps} />
{error && <span className="error">{error}</span>}
</div>
)
}
// Discriminated union for props
type AlertProps =
| { variant: 'success'; message: string }
| { variant: 'error'; message: string; onRetry: () => void }
| { variant: 'warning'; message: string; dismissible: boolean }
export const Alert: FC<AlertProps> = (props) => {
// WHY: TypeScript narrows type based on variant
switch (props.variant) {
case 'success':
return <div className="alert-success">{props.message}</div>
case 'error':
return (
<div className="alert-error">
{props.message}
<button onClick={props.onRetry}>Retry</button>
</div>
)
case 'warning':
return (
<div className="alert-warning">
{props.message}
{props.dismissible && <button>Dismiss</button>}
</div>
)
}
}
useState Hook
import { useState } from 'react'
export const Counter: FC = () => {
// Basic state
const [count, setCount] = useState(0)
// State with type inference
const [user, setUser] = useState<User | null>(null)
// State with function initializer (lazy initialization)
const [items, setItems] = useState(() => {
// WHY: Expensive computation runs only once
return loadItemsFromLocalStorage()
})
// Functional updates
const increment = () => {
// WHY: Use functional update when new state depends on previous state
setCount(prevCount => prevCount + 1)
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
)
}
useEffect Hook
import { useEffect, useState } from 'react'
export const UserProfile: FC<{ userId: string }> = ({ userId }) => {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
// Effect with cleanup
useEffect(() => {
let isMounted = true
const fetchUser = async () => {
setLoading(true)
try {
const data = await api.getUser(userId)
// WHY: Check if component is still mounted before updating state
if (isMounted) {
setUser(data)
}
} finally {
if (isMounted) {
setLoading(false)
}
}
}
fetchUser()
// Cleanup function
return () => {
isMounted = false
}
}, [userId]) // Dependency array
if (loading) return <div>Loading...</div>
if (!user) return <div>User not found</div>
return <div>{user.name}</div>
}
// Effect for event listeners
export const WindowSize: FC = () => {
const [size, setSize] = useState({ width: 0, height: 0 })
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
window.addEventListener('resize', handleResize)
handleResize() // Initial size
// WHY: Remove event listener on unmount to prevent memory leaks
return () => {
window.removeEventListener('resize', handleResize)
}
}, []) // Empty array - run once on mount
}
useContext Hook
import { createContext, useContext, FC, ReactNode } from 'react'
interface ThemeContextValue {
theme: 'light' | 'dark'
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined)
export const ThemeProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// Custom hook for consuming context
export const useTheme = (): ThemeContextValue => {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
// Usage in component
export const ThemedButton: FC = () => {
const { theme, toggleTheme } = useTheme()
return (
<button
onClick={toggleTheme}
className={theme === 'light' ? 'btn-light' : 'btn-dark'}
>
Toggle Theme
</button>
)
}
useReducer Hook
import { useReducer } from 'react'
type State = {
count: number
step: number
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'setStep'; payload: number }
| { type: 'reset' }
// WHY: Reducer pattern for complex state logic
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step }
case 'decrement':
return { ...state, count: state.count - state.step }
case 'setStep':
return { ...state, step: action.payload }
case 'reset':
return { count: 0, step: 1 }
default:
return state
}
}
export const Counter: FC = () => {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 })
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
<input
type="number"
value={state.step}
onChange={(e) => dispatch({ type: 'setStep', payload: Number(e.target.value) })}
/>
</div>
)
}
useRef Hook
import { useRef, useEffect } from 'react'
// Ref for DOM access
export const FocusInput: FC = () => {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
// WHY: Focus input on mount
inputRef.current?.focus()
}, [])
return <input ref={inputRef} />
}
// Ref for mutable value (doesn't trigger re-render)
export const Timer: FC = () => {
const [count, setCount] = useState(0)
const intervalRef = useRef<number | null>(null)
const start = () => {
if (intervalRef.current) return
// WHY: Store interval ID in ref to access in cleanup
intervalRef.current = window.setInterval(() => {
setCount(c => c + 1)
}, 1000)
}
const stop = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
useEffect(() => {
return () => stop() // Cleanup on unmount
}, [])
return (
<div>
<p>{count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
)
}
Custom Hooks
// Custom hook for fetching data
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let isMounted = true
const fetchData = async () => {
try {
setLoading(true)
const response = await fetch(url)
const json = await response.json()
if (isMounted) {
setData(json)
setError(null)
}
} catch (err) {
if (isMounted) {
setError(err instanceof Error ? err.message : 'Unknown error')
}
} finally {
if (isMounted) {
setLoading(false)
}
}
}
fetchData()
return () => {
isMounted = false
}
}, [url])
return { data, loading, error }
}
// Usage
export const UserList: FC = () => {
const { data: users, loading, error } = useFetch<User[]>('/api/users')
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
// Custom hook for local storage
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
// WHY: Sync with localStorage when value changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.error('Error saving to localStorage:', error)
}
}, [key, value])
return [value, setValue] as const
}
Performance Optimization
import { memo, useMemo, useCallback } from 'react'
// memo - prevent unnecessary re-renders
interface ItemProps {
id: string
name: string
onClick: (id: string) => void
}
// WHY: Component re-renders only when props change
export const Item = memo<ItemProps>(({ id, name, onClick }) => {
return (
<div onClick={() => onClick(id)}>
{name}
</div>
)
})
// useMemo - memoize expensive computations
export const ExpensiveList: FC<{ items: Item[] }> = ({ items }) => {
// WHY: Recalculate only when items change
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name))
}, [items])
return (
<ul>
{sortedItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
// useCallback - memoize function references
export const TodoList: FC = () => {
const [todos, setTodos] = useState<Todo[]>([])
// WHY: Prevent creating new function on every render
const handleToggle = useCallback((id: string) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
)
}, []) // No dependencies - function never changes
return (
<ul>
{todos.map(todo => (
<Item key={todo.id} {...todo} onClick={handleToggle} />
))}
</ul>
)
}
Component Composition
// Composition with children
interface CardProps {
children: React.ReactNode
}
export const Card: FC<CardProps> = ({ children }) => {
return <div className="card">{children}</div>
}
export const CardHeader: FC<CardProps> = ({ children }) => {
return <div className="card-header">{children}</div>
}
export const CardBody: FC<CardProps> = ({ children }) => {
return <div className="card-body">{children}</div>
}
// Usage
export const UserCard: FC = () => {
return (
<Card>
<CardHeader>User Profile</CardHeader>
<CardBody>
<p>Name: John Doe</p>
<p>Email: john@example.com</p>
</CardBody>
</Card>
)
}
// Compound Component pattern
interface TabsContextValue {
activeTab: string
setActiveTab: (tab: string) => void
}
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
export const Tabs: FC<{ children: ReactNode; defaultTab: string }> = ({
children,
defaultTab,
}) => {
const [activeTab, setActiveTab] = useState(defaultTab)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
)
}
export const TabList: FC<{ children: ReactNode }> = ({ children }) => {
return <div className="tab-list">{children}</div>
}
export const Tab: FC<{ value: string; children: ReactNode }> = ({ value, children }) => {
const context = useContext(TabsContext)
if (!context) throw new Error('Tab must be used within Tabs')
const { activeTab, setActiveTab } = context
const isActive = activeTab === value
return (
<button
className={isActive ? 'tab-active' : 'tab'}
onClick={() => setActiveTab(value)}
>
{children}
</button>
)
}
export const TabPanel: FC<{ value: string; children: ReactNode }> = ({ value, children }) => {
const context = useContext(TabsContext)
if (!context) throw new Error('TabPanel must be used within Tabs')
const { activeTab } = context
if (activeTab !== value) return null
return <div className="tab-panel">{children}</div>
}
// Usage
export const Dashboard: FC = () => {
return (
<Tabs defaultTab="overview">
<TabList>
<Tab value="overview">Overview</Tab>
<Tab value="analytics">Analytics</Tab>
<Tab value="settings">Settings</Tab>
</TabList>
<TabPanel value="overview">Overview content</TabPanel>
<TabPanel value="analytics">Analytics content</TabPanel>
<TabPanel value="settings">Settings content</TabPanel>
</Tabs>
)
}
Tools to Use
Read: Read existing React componentsWrite: Create new component filesEdit: Modify existing componentsBash: Run tests, type checker, and linters
Bash Commands
# Type checking
tsc --noEmit
# Run tests
vitest
vitest --ui
# Linting
eslint src/ --ext .tsx
# Formatting
prettier --write "src/**/*.tsx"
Workflow
- Understand Requirements: Clarify component requirements and API
- Write Tests First: Use
vitest-react-testingskill - Verify Tests Fail: Confirm tests fail correctly (Red)
- Define Props: Start with TypeScript interface for props
- Implement Component: Build component with hooks
- Run Tests: Ensure tests pass (Green)
- Run Type Checker: Ensure no type errors
- Optimize: Add memo, useMemo, useCallback if needed
- Refactor: Improve code quality
- Commit: Create atomic commit
Related Skills
typescript-core-development: For type definitionsreact-state-management: For complex state logicvitest-react-testing: For component testingstorybook-development: For component documentation
Coding Standards
See React Coding Standards
TDD Workflow
Follow Frontend TDD Workflow
Key Reminders
- Always use functional components with hooks
- Type all props with TypeScript interfaces
- Use built-in hooks before creating custom ones
- Extract reusable logic into custom hooks
- Use memo, useMemo, useCallback for performance (but don't over-optimize)
- Prefer composition over complex component hierarchies
- Clean up effects with return function
- Use functional setState updates when depending on previous state
- Keep components focused on single responsibility
- Write tests before implementation (TDD)
- Run type checker to catch errors early
- Write comments explaining WHY, not WHAT
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?