mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-14 07:13:35 +08:00
Several published examples contained APIs that no longer exist, code that does not run, or model versions that drifted from reality: - agents/performance-optimizer.md used the web-vitals v3 API (getCLS/getFID/getLCP/getFCP/getTTFB) and reported FID. web-vitals v4 renamed the imports to onCLS/onINP/onLCP/onFCP/onTTFB and FID was replaced by INP (target < 200ms) - rules/common/performance.md pinned stale model versions in the model-selection guidance; refresh to the versions the repo itself uses (agent.yaml pins claude-opus-4-6) and add the PowerShell variant for MAX_THINKING_TOKENS next to the bash export - skills/python-patterns/SKILL.md: both get_value examples referenced default_value without declaring the parameter (NameError); add default_value: Any = None to the EAFP and LBYL signatures - skills/frontend-patterns/SKILL.md: the custom useQuery example rebuilt refetch whenever callers passed inline fetchers/options, re-triggering the effect after every state update (infinite fetch loop). Keep the latest fetcher/options in refs so refetch stays referentially stable. The PASS-labelled useMemo example mutated its input with in-place sort; copy before sorting - skills/coding-standards/SKILL.md repeated the same PASS-labelled in-place-sort-in-useMemo example; same fix - rules/typescript/security.md used a vendor-specific OPENAI_API_KEY in generic guidance; switch to a neutral API_KEY Every hand-maintained copy of the affected content is synced in the same change: locale mirrors (ja-JP, ko-KR, pt-BR, tr, zh-CN, zh-TW - each only where it carries the affected file) and the .agents/.kiro/.cursor harness mirrors. Two structural divergences are left alone and noted here: .kiro/steering/performance.md has no extended-thinking control list to carry the PowerShell variant, and docs/zh-TW/rules/performance.md keeps an older condensed thinking section without the budget-cap line. rules/zh/performance.md is intentionally untouched - the rules/zh tree is being retired in a separate change
662 lines
16 KiB
Markdown
662 lines
16 KiB
Markdown
---
|
|
name: frontend-patterns
|
|
description: Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices.
|
|
---
|
|
|
|
# Frontend Development Patterns
|
|
|
|
Modern frontend patterns for React, Next.js, and performant user interfaces.
|
|
|
|
## When to Activate
|
|
|
|
- Building React components (composition, props, rendering)
|
|
- Managing state (useState, useReducer, Zustand, Context)
|
|
- Implementing data fetching (SWR, React Query, server components)
|
|
- Optimizing performance (memoization, virtualization, code splitting)
|
|
- Working with forms (validation, controlled inputs, Zod schemas)
|
|
- Handling client-side routing and navigation
|
|
- Building accessible, responsive UI patterns
|
|
|
|
## Privacy and Data Boundaries
|
|
|
|
Frontend examples should use synthetic or domain-generic data. Do not collect, log, persist, or display credentials, access tokens, SSNs, health data, payment details, private emails, phone numbers, or other sensitive personal data unless the user explicitly requests a scoped implementation with appropriate validation, redaction, and access controls.
|
|
|
|
Avoid adding analytics, tracking pixels, third-party scripts, or external data sinks without explicit approval. When handling user data, prefer least-privilege APIs, client-side redaction before logging, and server-side validation for every boundary.
|
|
|
|
## Component Patterns
|
|
|
|
### Composition Over Inheritance
|
|
|
|
```typescript
|
|
// PASS: GOOD: Component composition
|
|
interface CardProps {
|
|
children: React.ReactNode
|
|
variant?: 'default' | 'outlined'
|
|
}
|
|
|
|
export function Card({ children, variant = 'default' }: CardProps) {
|
|
return <div className={`card card-${variant}`}>{children}</div>
|
|
}
|
|
|
|
export function CardHeader({ children }: { children: React.ReactNode }) {
|
|
return <div className="card-header">{children}</div>
|
|
}
|
|
|
|
export function CardBody({ children }: { children: React.ReactNode }) {
|
|
return <div className="card-body">{children}</div>
|
|
}
|
|
|
|
// Usage
|
|
<Card>
|
|
<CardHeader>Title</CardHeader>
|
|
<CardBody>Content</CardBody>
|
|
</Card>
|
|
```
|
|
|
|
### Compound Components
|
|
|
|
```typescript
|
|
interface TabsContextValue {
|
|
activeTab: string
|
|
setActiveTab: (tab: string) => void
|
|
}
|
|
|
|
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
|
|
|
|
export function Tabs({ children, defaultTab }: {
|
|
children: React.ReactNode
|
|
defaultTab: string
|
|
}) {
|
|
const [activeTab, setActiveTab] = useState(defaultTab)
|
|
|
|
return (
|
|
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
|
|
{children}
|
|
</TabsContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function TabList({ children }: { children: React.ReactNode }) {
|
|
return <div className="tab-list">{children}</div>
|
|
}
|
|
|
|
export function Tab({ id, children }: { id: string, children: React.ReactNode }) {
|
|
const context = useContext(TabsContext)
|
|
if (!context) throw new Error('Tab must be used within Tabs')
|
|
|
|
return (
|
|
<button
|
|
className={context.activeTab === id ? 'active' : ''}
|
|
onClick={() => context.setActiveTab(id)}
|
|
>
|
|
{children}
|
|
</button>
|
|
)
|
|
}
|
|
|
|
// Usage
|
|
<Tabs defaultTab="overview">
|
|
<TabList>
|
|
<Tab id="overview">Overview</Tab>
|
|
<Tab id="details">Details</Tab>
|
|
</TabList>
|
|
</Tabs>
|
|
```
|
|
|
|
### Render Props Pattern
|
|
|
|
```typescript
|
|
interface DataLoaderProps<T> {
|
|
url: string
|
|
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode
|
|
}
|
|
|
|
export function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
|
|
const [data, setData] = useState<T | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<Error | null>(null)
|
|
|
|
useEffect(() => {
|
|
fetch(url)
|
|
.then(res => res.json())
|
|
.then(setData)
|
|
.catch(setError)
|
|
.finally(() => setLoading(false))
|
|
}, [url])
|
|
|
|
return <>{children(data, loading, error)}</>
|
|
}
|
|
|
|
// Usage
|
|
<DataLoader<Market[]> url="/api/markets">
|
|
{(markets, loading, error) => {
|
|
if (loading) return <Spinner />
|
|
if (error) return <Error error={error} />
|
|
return <MarketList markets={markets!} />
|
|
}}
|
|
</DataLoader>
|
|
```
|
|
|
|
## Custom Hooks Patterns
|
|
|
|
### State Management Hook
|
|
|
|
```typescript
|
|
export function useToggle(initialValue = false): [boolean, () => void] {
|
|
const [value, setValue] = useState(initialValue)
|
|
|
|
const toggle = useCallback(() => {
|
|
setValue(v => !v)
|
|
}, [])
|
|
|
|
return [value, toggle]
|
|
}
|
|
|
|
// Usage
|
|
const [isOpen, toggleOpen] = useToggle()
|
|
```
|
|
|
|
### Async Data Fetching Hook
|
|
|
|
```typescript
|
|
interface UseQueryOptions<T> {
|
|
onSuccess?: (data: T) => void
|
|
onError?: (error: Error) => void
|
|
enabled?: boolean
|
|
}
|
|
|
|
export function useQuery<T>(
|
|
key: string,
|
|
fetcher: () => Promise<T>,
|
|
options?: UseQueryOptions<T>
|
|
) {
|
|
const [data, setData] = useState<T | null>(null)
|
|
const [error, setError] = useState<Error | null>(null)
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
// Keep the latest fetcher/options in refs so refetch stays referentially
|
|
// stable even when callers pass inline functions and object literals.
|
|
// Without this, every render creates a new refetch, and the effect below
|
|
// re-runs after each state update - an infinite fetch loop.
|
|
const fetcherRef = useRef(fetcher)
|
|
const optionsRef = useRef(options)
|
|
useEffect(() => {
|
|
fetcherRef.current = fetcher
|
|
optionsRef.current = options
|
|
})
|
|
|
|
const refetch = useCallback(async () => {
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const result = await fetcherRef.current()
|
|
setData(result)
|
|
optionsRef.current?.onSuccess?.(result)
|
|
} catch (err) {
|
|
const error = err as Error
|
|
setError(error)
|
|
optionsRef.current?.onError?.(error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
const enabled = options?.enabled !== false
|
|
|
|
useEffect(() => {
|
|
if (enabled) {
|
|
refetch()
|
|
}
|
|
}, [key, enabled, refetch])
|
|
|
|
return { data, error, loading, refetch }
|
|
}
|
|
|
|
// Usage
|
|
const { data: markets, loading, error, refetch } = useQuery(
|
|
'markets',
|
|
() => fetch('/api/markets').then(r => r.json()),
|
|
{
|
|
onSuccess: data => console.log('Fetched', data.length, 'markets'),
|
|
onError: err => console.error('Failed:', err)
|
|
}
|
|
)
|
|
```
|
|
|
|
### Debounce Hook
|
|
|
|
```typescript
|
|
export function useDebounce<T>(value: T, delay: number): T {
|
|
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
|
|
|
useEffect(() => {
|
|
const handler = setTimeout(() => {
|
|
setDebouncedValue(value)
|
|
}, delay)
|
|
|
|
return () => clearTimeout(handler)
|
|
}, [value, delay])
|
|
|
|
return debouncedValue
|
|
}
|
|
|
|
// Usage
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
const debouncedQuery = useDebounce(searchQuery, 500)
|
|
|
|
useEffect(() => {
|
|
if (debouncedQuery) {
|
|
performSearch(debouncedQuery)
|
|
}
|
|
}, [debouncedQuery])
|
|
```
|
|
|
|
## State Management Patterns
|
|
|
|
### Context + Reducer Pattern
|
|
|
|
```typescript
|
|
interface State {
|
|
markets: Market[]
|
|
selectedMarket: Market | null
|
|
loading: boolean
|
|
}
|
|
|
|
type Action =
|
|
| { type: 'SET_MARKETS'; payload: Market[] }
|
|
| { type: 'SELECT_MARKET'; payload: Market }
|
|
| { type: 'SET_LOADING'; payload: boolean }
|
|
|
|
function reducer(state: State, action: Action): State {
|
|
switch (action.type) {
|
|
case 'SET_MARKETS':
|
|
return { ...state, markets: action.payload }
|
|
case 'SELECT_MARKET':
|
|
return { ...state, selectedMarket: action.payload }
|
|
case 'SET_LOADING':
|
|
return { ...state, loading: action.payload }
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
const MarketContext = createContext<{
|
|
state: State
|
|
dispatch: Dispatch<Action>
|
|
} | undefined>(undefined)
|
|
|
|
export function MarketProvider({ children }: { children: React.ReactNode }) {
|
|
const [state, dispatch] = useReducer(reducer, {
|
|
markets: [],
|
|
selectedMarket: null,
|
|
loading: false
|
|
})
|
|
|
|
return (
|
|
<MarketContext.Provider value={{ state, dispatch }}>
|
|
{children}
|
|
</MarketContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function useMarkets() {
|
|
const context = useContext(MarketContext)
|
|
if (!context) throw new Error('useMarkets must be used within MarketProvider')
|
|
return context
|
|
}
|
|
```
|
|
|
|
## Performance Optimization
|
|
|
|
### Memoization
|
|
|
|
```typescript
|
|
// PASS: useMemo for expensive computations
|
|
// Copy before sorting - Array.prototype.sort mutates in place
|
|
const sortedMarkets = useMemo(() => {
|
|
return [...markets].sort((a, b) => b.volume - a.volume)
|
|
}, [markets])
|
|
|
|
// PASS: useCallback for functions passed to children
|
|
const handleSearch = useCallback((query: string) => {
|
|
setSearchQuery(query)
|
|
}, [])
|
|
|
|
// PASS: React.memo for pure components
|
|
export const MarketCard = React.memo<MarketCardProps>(({ market }) => {
|
|
return (
|
|
<div className="market-card">
|
|
<h3>{market.name}</h3>
|
|
<p>{market.description}</p>
|
|
</div>
|
|
)
|
|
})
|
|
```
|
|
|
|
### Code Splitting & Lazy Loading
|
|
|
|
```typescript
|
|
import { lazy, Suspense } from 'react'
|
|
|
|
// PASS: Lazy load heavy components
|
|
const HeavyChart = lazy(() => import('./HeavyChart'))
|
|
const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'))
|
|
|
|
export function Dashboard() {
|
|
return (
|
|
<div>
|
|
<Suspense fallback={<ChartSkeleton />}>
|
|
<HeavyChart data={data} />
|
|
</Suspense>
|
|
|
|
<Suspense fallback={null}>
|
|
<ThreeJsBackground />
|
|
</Suspense>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Virtualization for Long Lists
|
|
|
|
```typescript
|
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
|
|
|
export function VirtualMarketList({ markets }: { markets: Market[] }) {
|
|
const parentRef = useRef<HTMLDivElement>(null)
|
|
|
|
const virtualizer = useVirtualizer({
|
|
count: markets.length,
|
|
getScrollElement: () => parentRef.current,
|
|
estimateSize: () => 100, // Estimated row height
|
|
overscan: 5 // Extra items to render
|
|
})
|
|
|
|
return (
|
|
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
|
|
<div
|
|
style={{
|
|
height: `${virtualizer.getTotalSize()}px`,
|
|
position: 'relative'
|
|
}}
|
|
>
|
|
{virtualizer.getVirtualItems().map(virtualRow => (
|
|
<div
|
|
key={virtualRow.index}
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
height: `${virtualRow.size}px`,
|
|
transform: `translateY(${virtualRow.start}px)`
|
|
}}
|
|
>
|
|
<MarketCard market={markets[virtualRow.index]} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Form Handling Patterns
|
|
|
|
### Controlled Form with Validation
|
|
|
|
```typescript
|
|
interface FormData {
|
|
name: string
|
|
description: string
|
|
endDate: string
|
|
}
|
|
|
|
interface FormErrors {
|
|
name?: string
|
|
description?: string
|
|
endDate?: string
|
|
}
|
|
|
|
export function CreateMarketForm() {
|
|
const [formData, setFormData] = useState<FormData>({
|
|
name: '',
|
|
description: '',
|
|
endDate: ''
|
|
})
|
|
|
|
const [errors, setErrors] = useState<FormErrors>({})
|
|
|
|
const validate = (): boolean => {
|
|
const newErrors: FormErrors = {}
|
|
|
|
if (!formData.name.trim()) {
|
|
newErrors.name = 'Name is required'
|
|
} else if (formData.name.length > 200) {
|
|
newErrors.name = 'Name must be under 200 characters'
|
|
}
|
|
|
|
if (!formData.description.trim()) {
|
|
newErrors.description = 'Description is required'
|
|
}
|
|
|
|
if (!formData.endDate) {
|
|
newErrors.endDate = 'End date is required'
|
|
}
|
|
|
|
setErrors(newErrors)
|
|
return Object.keys(newErrors).length === 0
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
if (!validate()) return
|
|
|
|
try {
|
|
await createMarket(formData)
|
|
// Success handling
|
|
} catch (error) {
|
|
// Error handling
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit}>
|
|
<input
|
|
value={formData.name}
|
|
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
placeholder="Market name"
|
|
/>
|
|
{errors.name && <span className="error">{errors.name}</span>}
|
|
|
|
{/* Other fields */}
|
|
|
|
<button type="submit">Create Market</button>
|
|
</form>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Error Boundary Pattern
|
|
|
|
```typescript
|
|
interface ErrorBoundaryState {
|
|
hasError: boolean
|
|
error: Error | null
|
|
}
|
|
|
|
export class ErrorBoundary extends React.Component<
|
|
{ children: React.ReactNode },
|
|
ErrorBoundaryState
|
|
> {
|
|
state: ErrorBoundaryState = {
|
|
hasError: false,
|
|
error: null
|
|
}
|
|
|
|
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
return { hasError: true, error }
|
|
}
|
|
|
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
console.error('Error boundary caught:', error, errorInfo)
|
|
}
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
return (
|
|
<div className="error-fallback">
|
|
<h2>Something went wrong</h2>
|
|
<p>{this.state.error?.message}</p>
|
|
<button onClick={() => this.setState({ hasError: false })}>
|
|
Try again
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return this.props.children
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
<ErrorBoundary>
|
|
<App />
|
|
</ErrorBoundary>
|
|
```
|
|
|
|
## Animation Patterns
|
|
|
|
### Framer Motion Animations
|
|
|
|
```typescript
|
|
import { motion, AnimatePresence } from 'framer-motion'
|
|
|
|
// PASS: List animations
|
|
export function AnimatedMarketList({ markets }: { markets: Market[] }) {
|
|
return (
|
|
<AnimatePresence>
|
|
{markets.map(market => (
|
|
<motion.div
|
|
key={market.id}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<MarketCard market={market} />
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
)
|
|
}
|
|
|
|
// PASS: Modal animations
|
|
export function Modal({ isOpen, onClose, children }: ModalProps) {
|
|
return (
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<>
|
|
<motion.div
|
|
className="modal-overlay"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
onClick={onClose}
|
|
/>
|
|
<motion.div
|
|
className="modal-content"
|
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
</>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Accessibility Patterns
|
|
|
|
### Keyboard Navigation
|
|
|
|
```typescript
|
|
export function Dropdown({ options, onSelect }: DropdownProps) {
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
const [activeIndex, setActiveIndex] = useState(0)
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
e.preventDefault()
|
|
setActiveIndex(i => Math.min(i + 1, options.length - 1))
|
|
break
|
|
case 'ArrowUp':
|
|
e.preventDefault()
|
|
setActiveIndex(i => Math.max(i - 1, 0))
|
|
break
|
|
case 'Enter':
|
|
e.preventDefault()
|
|
onSelect(options[activeIndex])
|
|
setIsOpen(false)
|
|
break
|
|
case 'Escape':
|
|
setIsOpen(false)
|
|
break
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
role="combobox"
|
|
aria-expanded={isOpen}
|
|
aria-haspopup="listbox"
|
|
onKeyDown={handleKeyDown}
|
|
>
|
|
{/* Dropdown implementation */}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Focus Management
|
|
|
|
```typescript
|
|
export function Modal({ isOpen, onClose, children }: ModalProps) {
|
|
const modalRef = useRef<HTMLDivElement>(null)
|
|
const previousFocusRef = useRef<HTMLElement | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
// Save currently focused element
|
|
previousFocusRef.current = document.activeElement as HTMLElement
|
|
|
|
// Focus modal
|
|
modalRef.current?.focus()
|
|
} else {
|
|
// Restore focus when closing
|
|
previousFocusRef.current?.focus()
|
|
}
|
|
}, [isOpen])
|
|
|
|
return isOpen ? (
|
|
<div
|
|
ref={modalRef}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
tabIndex={-1}
|
|
onKeyDown={e => e.key === 'Escape' && onClose()}
|
|
>
|
|
{children}
|
|
</div>
|
|
) : null
|
|
}
|
|
```
|
|
|
|
**Remember**: Modern frontend patterns enable maintainable, performant user interfaces. Choose patterns that fit your project complexity.
|