frontend-patterns

Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices.

INSTALLATION
npx skills add https://github.com/affaan-m/ecc --skill frontend-patterns
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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

// 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

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

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

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<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

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

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

// 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

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

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

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

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

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

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

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.

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card