mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-01 06:23:28 +08:00
648 lines
15 KiB
Markdown
648 lines
15 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)
|
|
|
|
const refetch = useCallback(async () => {
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const result = await fetcher()
|
|
setData(result)
|
|
options?.onSuccess?.(result)
|
|
} catch (err) {
|
|
const error = err as Error
|
|
setError(error)
|
|
options?.onError?.(error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [fetcher, options])
|
|
|
|
useEffect(() => {
|
|
if (options?.enabled !== false) {
|
|
refetch()
|
|
}
|
|
}, [key, refetch, options?.enabled])
|
|
|
|
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
|
|
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.
|