Frontend Patterns
/install frontend-patterns
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
Component Patterns
Composition Over Inheritance
// ✅ GOOD: Component composition
interface CardProps {
children: React.ReactNode
variant?: 'default' | 'outlined'
}
export function Card({ children, variant = 'default' }: CardProps) {
return \x3Cdiv className={`card card-${variant}`}>{children}\x3C/div>
}
export function CardHeader({ children }: { children: React.ReactNode }) {
return \x3Cdiv className="card-header">{children}\x3C/div>
}
export function CardBody({ children }: { children: React.ReactNode }) {
return \x3Cdiv className="card-body">{children}\x3C/div>
}
// Usage
\x3CCard>
\x3CCardHeader>Title\x3C/CardHeader>
\x3CCardBody>Content\x3C/CardBody>
\x3C/Card>
Compound Components
interface TabsContextValue {
activeTab: string
setActiveTab: (tab: string) => void
}
const TabsContext = createContext\x3CTabsContextValue | undefined>(undefined)
export function Tabs({ children, defaultTab }: {
children: React.ReactNode
defaultTab: string
}) {
const [activeTab, setActiveTab] = useState(defaultTab)
return (
\x3CTabsContext.Provider value={{ activeTab, setActiveTab }}>
{children}
\x3C/TabsContext.Provider>
)
}
export function TabList({ children }: { children: React.ReactNode }) {
return \x3Cdiv className="tab-list">{children}\x3C/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 (
\x3Cbutton
className={context.activeTab === id ? 'active' : ''}
onClick={() => context.setActiveTab(id)}
>
{children}
\x3C/button>
)
}
// Usage
\x3CTabs defaultTab="overview">
\x3CTabList>
\x3CTab id="overview">Overview\x3C/Tab>
\x3CTab id="details">Details\x3C/Tab>
\x3C/TabList>
\x3C/Tabs>
Render Props Pattern
interface DataLoaderProps\x3CT> {
url: string
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode
}
export function DataLoader\x3CT>({ url, children }: DataLoaderProps\x3CT>) {
const [data, setData] = useState\x3CT | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState\x3CError | null>(null)
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [url])
return \x3C>{children(data, loading, error)}\x3C/>
}
// Usage
\x3CDataLoader\x3CMarket[]> url="/api/markets">
{(markets, loading, error) => {
if (loading) return \x3CSpinner />
if (error) return \x3CError error={error} />
return \x3CMarketList markets={markets!} />
}}
\x3C/DataLoader>
Custom Hooks Patterns
State Management Hook
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
interface UseQueryOptions\x3CT> {
onSuccess?: (data: T) => void
onError?: (error: Error) => void
enabled?: boolean
}
export function useQuery\x3CT>(
key: string,
fetcher: () => Promise\x3CT>,
options?: UseQueryOptions\x3CT>
) {
const [data, setData] = useState\x3CT | null>(null)
const [error, setError] = useState\x3CError | 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
export function useDebounce\x3CT>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState\x3CT>(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
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\x3C{
state: State
dispatch: Dispatch\x3CAction>
} | undefined>(undefined)
export function MarketProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, {
markets: [],
selectedMarket: null,
loading: false
})
return (
\x3CMarketContext.Provider value={{ state, dispatch }}>
{children}
\x3C/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
// ✅ useMemo for expensive computations
const sortedMarkets = useMemo(() => {
return markets.sort((a, b) => b.volume - a.volume)
}, [markets])
// ✅ useCallback for functions passed to children
const handleSearch = useCallback((query: string) => {
setSearchQuery(query)
}, [])
// ✅ React.memo for pure components
export const MarketCard = React.memo\x3CMarketCardProps>(({ market }) => {
return (
\x3Cdiv className="market-card">
\x3Ch3>{market.name}\x3C/h3>
\x3Cp>{market.description}\x3C/p>
\x3C/div>
)
})
Code Splitting & Lazy Loading
import { lazy, Suspense } from 'react'
// ✅ Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'))
const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'))
export function Dashboard() {
return (
\x3Cdiv>
\x3CSuspense fallback={\x3CChartSkeleton />}>
\x3CHeavyChart data={data} />
\x3C/Suspense>
\x3CSuspense fallback={null}>
\x3CThreeJsBackground />
\x3C/Suspense>
\x3C/div>
)
}
Virtualization for Long Lists
import { useVirtualizer } from '@tanstack/react-virtual'
export function VirtualMarketList({ markets }: { markets: Market[] }) {
const parentRef = useRef\x3CHTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: markets.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100, // Estimated row height
overscan: 5 // Extra items to render
})
return (
\x3Cdiv ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
\x3Cdiv
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative'
}}
>
{virtualizer.getVirtualItems().map(virtualRow => (
\x3Cdiv
key={virtualRow.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`
}}
>
\x3CMarketCard market={markets[virtualRow.index]} />
\x3C/div>
))}
\x3C/div>
\x3C/div>
)
}
Form Handling Patterns
Controlled Form with Validation
interface FormData {
name: string
description: string
endDate: string
}
interface FormErrors {
name?: string
description?: string
endDate?: string
}
export function CreateMarketForm() {
const [formData, setFormData] = useState\x3CFormData>({
name: '',
description: '',
endDate: ''
})
const [errors, setErrors] = useState\x3CFormErrors>({})
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 (
\x3Cform onSubmit={handleSubmit}>
\x3Cinput
value={formData.name}
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Market name"
/>
{errors.name && \x3Cspan className="error">{errors.name}\x3C/span>}
{/* Other fields */}
\x3Cbutton type="submit">Create Market\x3C/button>
\x3C/form>
)
}
Error Boundary Pattern
interface ErrorBoundaryState {
hasError: boolean
error: Error | null
}
export class ErrorBoundary extends React.Component\x3C
{ 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 (
\x3Cdiv className="error-fallback">
\x3Ch2>Something went wrong\x3C/h2>
\x3Cp>{this.state.error?.message}\x3C/p>
\x3Cbutton onClick={() => this.setState({ hasError: false })}>
Try again
\x3C/button>
\x3C/div>
)
}
return this.props.children
}
}
// Usage
\x3CErrorBoundary>
\x3CApp />
\x3C/ErrorBoundary>
Animation Patterns
Framer Motion Animations
import { motion, AnimatePresence } from 'framer-motion'
// ✅ List animations
export function AnimatedMarketList({ markets }: { markets: Market[] }) {
return (
\x3CAnimatePresence>
{markets.map(market => (
\x3Cmotion.div
key={market.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
\x3CMarketCard market={market} />
\x3C/motion.div>
))}
\x3C/AnimatePresence>
)
}
// ✅ Modal animations
export function Modal({ isOpen, onClose, children }: ModalProps) {
return (
\x3CAnimatePresence>
{isOpen && (
\x3C>
\x3Cmotion.div
className="modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
\x3Cmotion.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}
\x3C/motion.div>
\x3C/>
)}
\x3C/AnimatePresence>
)
}
Accessibility Patterns
Keyboard Navigation
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 (
\x3Cdiv
role="combobox"
aria-expanded={isOpen}
aria-haspopup="listbox"
onKeyDown={handleKeyDown}
>
{/* Dropdown implementation */}
\x3C/div>
)
}
Focus Management
export function Modal({ isOpen, onClose, children }: ModalProps) {
const modalRef = useRef\x3CHTMLDivElement>(null)
const previousFocusRef = useRef\x3CHTMLElement | 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 ? (
\x3Cdiv
ref={modalRef}
role="dialog"
aria-modal="true"
tabIndex={-1}
onKeyDown={e => e.key === 'Escape' && onClose()}
>
{children}
\x3C/div>
) : null
}
Remember: Modern frontend patterns enable maintainable, performant user interfaces. Choose patterns that fit your project complexity.
- Make sure OpenClaw is installed (local or Docker)
- Run the install command in chat:
/install frontend-patterns - After installation, invoke the skill by name or use
/frontend-patterns - Provide required inputs per the skill's parameter spec and get structured output
What is Frontend Patterns?
Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices. It is an AI Agent Skill for Claude Code / OpenClaw, with 265 downloads so far.
How do I install Frontend Patterns?
Run "/install frontend-patterns" in the OpenClaw or Claude Code chat to install it in one step — no extra setup required.
Is Frontend Patterns free?
Yes, Frontend Patterns is completely free, licensed under MIT-0. You can download, install and use it at no cost.
Which platforms does Frontend Patterns support?
Frontend Patterns is cross-platform and runs anywhere OpenClaw / Claude Code is available (cross-platform).
Who created Frontend Patterns?
It is built and maintained by npjameszheng1125-netizen (@npjameszheng1125-netizen); the current version is v1.0.0.