agentsclimarketplace

Frontend patterns

Skill Lukk17/agent-standards/.agents/skills/frontend-patterns

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

Install
npx -y skills add Lukk17/agent-standards --skill frontend-patterns

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

19.2 KB, ~4.7k tokens by cl100k_base, as published. Nobody here has run it

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

Define top-level components as named function declarations (function Card(...) {}). Reserve arrow functions for callbacks and inline handlers. Named declarations hoist, give clearer stack traces and component names in DevTools, and read consistently across a file.

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

Async state is one value, not three loose flags. Model it as a discriminated union so impossible combinations (loading and error and data all set at once) cannot be represented, and switch on state.status when rendering.

type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; reason: Error }

Data is fetched through the typed API client (see API Client below), never fetch directly.

interface DataLoaderProps<T> {
  load: () => Promise<T>
  children: (state: AsyncState<T>) => React.ReactNode
}

export function DataLoader<T>({ load, children }: DataLoaderProps<T>) {
  const [state, setState] = useState<AsyncState<T>>({ status: 'loading' })

  useEffect(() => {
    let active = true
    load()
      .then(data => active && setState({ status: 'success', data }))
      .catch((reason: Error) => active && setState({ status: 'error', reason }))
    return () => { active = false }
  }, [load])

  return <>{children(state)}</>
}

// Usage: the loader calls the API client, never fetch
<DataLoader<Market[]> load={() => api.markets.list()}>
  {state => {
    switch (state.status) {
      case 'loading':
        return <Spinner />
      case 'error':
        return <Error error={state.reason} />
      case 'success':
        return <MarketList markets={state.data} />
      default:
        return null
    }
  }}
</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

The hook exposes one state value of the AsyncState<T> discriminated union (see Render Props above), not separate data / loading / error flags. Callers switch on state.status. The fetcher is the typed API client (see API Client below), never a raw fetch.

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 [state, setState] = useState<AsyncState<T>>({ status: 'idle' })

  const refetch = useCallback(async () => {
    setState({ status: 'loading' })

    try {
      const data = await fetcher()
      setState({ status: 'success', data })
      options?.onSuccess?.(data)
    } catch (err) {
      const reason = err as Error
      setState({ status: 'error', reason })
      options?.onError?.(reason)
    }
  }, [fetcher, options])

  useEffect(() => {
    if (options?.enabled !== false) {
      refetch()
    }
  }, [key, refetch, options?.enabled])

  return { state, refetch }
}

// Usage: the fetcher is the API client, and rendering switches on state.status
const { state, refetch } = useQuery(
  'markets',
  () => api.markets.list(),
  {
    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])

API Client

All network calls go through one typed API client module. Components and hooks call the client; they never call fetch directly. This keeps the base URL, headers, auth, and response typing in one place and keeps endpoint URLs out of the UI.

// lib/api/client.ts
async function request<T>(path: string): Promise<T> {
  const res = await fetch(`${API_BASE_URL}${path}`)
  if (!res.ok) throw new Error(`Request failed: ${res.status}`)
  return res.json() as Promise<T>
}

export const api = {
  markets: {
    list: () => request<Market[]>('/markets'),
    get: (id: string) => request<Market>(`/markets/${id}`)
  }
}

// Usage: hooks and components call the client, never fetch
const markets = await api.markets.list()

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

Reserve useMemo, useCallback, and React.memo for hotspots you have measured. Each one adds a dependency array to maintain and a cache to hold in memory, and a wrong dependency list is its own class of bug. Reaching for them reflexively on cheap work costs more than it saves. Memoize only after a profile shows the work is expensive or the re-renders are the actual bottleneck.

Sorting and other transforms must not mutate their input. Array.prototype.sort sorts in place, so copy first with [...markets] before sorting; otherwise the source array (often a prop) is mutated.

// PASS: useMemo for a measured expensive computation, copy before sorting
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)
}, [])

MarketCard below is a trivial presentational component, so it is a plain function and is not wrapped in React.memo. Add React.memo only when a profile shows this component re-renders in a hot path with stable props.

export function MarketCard({ market }: MarketCardProps) {
  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={t('markets.form.namePlaceholder')}
      />
      {errors.name && <span className="error">{errors.name}</span>}

      {/* Other fields */}

      <button type="submit">{t('markets.form.submit')}</button>
    </form>
  )
}

Internationalization

User-facing text never lives as a literal in JSX. Every label, placeholder, button, and message goes through one i18n layer, looked up by key with t('...'), so copy lives in translation catalogues and the UI stays translatable. The examples above use this: t('markets.form.submit'), t('errors.generic'), t('errors.retry').

import { useTranslation } from 'react-i18next'

export function SaveButton() {
  const { t } = useTranslation()
  return <button type="submit">{t('common.save')}</button>
}

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>{t('errors.generic')}</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false })}>
            {t('errors.retry')}
          </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.


Motion & Animation Standards

prefers-reduced-motion (Required on ALL Animations)

Every animation or transition, whether CSS, JS, or a library, must respect the user's motion preference:

// PASS: GOOD — CSS approach
const styles = `
  .slide-in {
    animation: slideIn 300ms ease-out;
  }

  @media (prefers-reduced-motion: reduce) {
    .slide-in {
      animation: none;
    }
  }
`;

// PASS: GOOD — React/Framer Motion approach
import { useReducedMotion } from 'framer-motion'

export function AnimatedCard({ children }: { children: React.ReactNode }) {
  const shouldReduceMotion = useReducedMotion()

  return (
    <motion.div
      initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
    >
      {children}
    </motion.div>
  )
}

// FAIL: BAD — no motion preference check
export function AnimatedCard({ children }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
    >
      {children}
    </motion.div>
  )
}

Timing guidelines:

TypeDuration
Micro-interactions (hover, focus)100-150ms
Component transitions (modal, drawer)200-300ms
Page/route transitions300-500ms
Maximum allowed500ms

CSS Custom Property Animations

/* PASS: GOOD — token-driven, motion-safe */
.btn {
  transition: background-color var(--duration-fast, 150ms) ease;
}

@media (prefers-reduced-motion: reduce) {
  .btn { transition: none; }
}

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,852. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.