FoundryKit

Advanced Patterns

Advanced patterns and techniques for using FoundryKit components

Advanced Patterns

This guide covers advanced patterns and techniques for using FoundryKit components in complex scenarios.

State Management Patterns

Centralized Search State

Manage search state across multiple components:

import { createContext, useContext, useState, useCallback } from 'react'
import { SearchBar, Calendar, Pagination } from '@foundrykit/components'
import { Card, CardContent, CardHeader, CardTitle } from '@foundrykit/primitives'

interface SearchState {
  query: string
  date: Date | undefined
  page: number
  filters: Record<string, any>
}

interface SearchContextType {
  searchState: SearchState
  updateQuery: (query: string) => void
  updateDate: (date: Date | undefined) => void
  updatePage: (page: number) => void
  updateFilters: (filters: Record<string, any>) => void
  resetSearch: () => void
}

const SearchContext = createContext<SearchContextType | undefined>(undefined)

function SearchProvider({ children }: { children: React.ReactNode }) {
  const [searchState, setSearchState] = useState<SearchState>({
    query: '',
    date: undefined,
    page: 1,
    filters: {}
  })

  const updateQuery = useCallback((query: string) => {
    setSearchState(prev => ({ ...prev, query, page: 1 }))
  }, [])

  const updateDate = useCallback((date: Date | undefined) => {
    setSearchState(prev => ({ ...prev, date, page: 1 }))
  }, [])

  const updatePage = useCallback((page: number) => {
    setSearchState(prev => ({ ...prev, page }))
  }, [])

  const updateFilters = useCallback((filters: Record<string, any>) => {
    setSearchState(prev => ({ ...prev, filters, page: 1 }))
  }, [])

  const resetSearch = useCallback(() => {
    setSearchState({
      query: '',
      date: undefined,
      page: 1,
      filters: {}
    })
  }, [])

  return (
    <SearchContext.Provider value={{
      searchState,
      updateQuery,
      updateDate,
      updatePage,
      updateFilters,
      resetSearch
    }}>
      {children}
    </SearchContext.Provider>
  )
}

function useSearch() {
  const context = useContext(SearchContext)
  if (!context) {
    throw new Error('useSearch must be used within SearchProvider')
  }
  return context
}

function AdvancedSearchInterface() {
  const { searchState, updateQuery, updateDate, updatePage, resetSearch } = useSearch()

  return (
    <div className="space-y-6">
      <Card>
        <CardHeader>
          <CardTitle>Advanced Search</CardTitle>
        </CardHeader>
        <CardContent className="space-y-4">
          <SearchBar
            placeholder="Search..."
            onSearch={updateQuery}
          />
          
          <Calendar
            mode="single"
            selected={searchState.date}
            onSelect={updateDate}
            className="rounded-md border"
          />
          
          <div className="flex space-x-2">
            <Button onClick={resetSearch}>Reset</Button>
          </div>
        </CardContent>
      </Card>
      
      <Pagination
        currentPage={searchState.page}
        totalPages={10}
        onPageChange={updatePage}
      />
    </div>
  )
}

Optimistic Updates

Implement optimistic updates for better user experience:

import { useState, useCallback } from 'react'
import { SearchBar, Calendar } from '@foundrykit/components'
import { Button, Card, CardContent } from '@foundrykit/primitives'

function OptimisticSearch() {
  const [searchState, setSearchState] = useState({
    query: '',
    date: undefined as Date | undefined
  })
  const [optimisticState, setOptimisticState] = useState(searchState)
  const [isUpdating, setIsUpdating] = useState(false)

  const handleSearch = useCallback(async (query: string) => {
    // Optimistically update UI
    setOptimisticState(prev => ({ ...prev, query }))
    
    try {
      setIsUpdating(true)
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 1000))
      
      // Update actual state
      setSearchState(prev => ({ ...prev, query }))
    } catch (error) {
      // Revert optimistic update on error
      setOptimisticState(searchState)
      console.error('Search failed:', error)
    } finally {
      setIsUpdating(false)
    }
  }, [searchState])

  const handleDateSelect = useCallback(async (date: Date | undefined) => {
    setOptimisticState(prev => ({ ...prev, date }))
    
    try {
      setIsUpdating(true)
      await new Promise(resolve => setTimeout(resolve, 500))
      setSearchState(prev => ({ ...prev, date }))
    } catch (error) {
      setOptimisticState(searchState)
      console.error('Date selection failed:', error)
    } finally {
      setIsUpdating(false)
    }
  }, [searchState])

  return (
    <Card>
      <CardContent className="space-y-4">
        <SearchBar
          placeholder="Search..."
          onSearch={handleSearch}
          disabled={isUpdating}
        />
        
        <Calendar
          mode="single"
          selected={optimisticState.date}
          onSelect={handleDateSelect}
          disabled={isUpdating}
        />
        
        {isUpdating && (
          <div className="text-sm text-muted-foreground">
            Updating...
          </div>
        )}
      </CardContent>
    </Card>
  )
}

Performance Patterns

Implement virtual scrolling for large datasets with search:

import { useState, useMemo, useCallback } from 'react'
import { SearchBar, Pagination } from '@foundrykit/components'
import { Card, CardContent } from '@foundrykit/primitives'

interface VirtualListProps {
  items: any[]
  itemHeight: number
  containerHeight: number
  renderItem: (item: any, index: number) => React.ReactNode
}

function VirtualList({ items, itemHeight, containerHeight, renderItem }: VirtualListProps) {
  const [scrollTop, setScrollTop] = useState(0)

  const visibleItems = useMemo(() => {
    const startIndex = Math.floor(scrollTop / itemHeight)
    const endIndex = Math.min(
      startIndex + Math.ceil(containerHeight / itemHeight) + 1,
      items.length
    )
    
    return items.slice(startIndex, endIndex).map((item, index) => ({
      item,
      index: startIndex + index
    }))
  }, [items, scrollTop, itemHeight, containerHeight])

  const totalHeight = items.length * itemHeight
  const offsetY = Math.floor(scrollTop / itemHeight) * itemHeight

  return (
    <div
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map(({ item, index }) => (
            <div key={index} style={{ height: itemHeight }}>
              {renderItem(item, index)}
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

function VirtualSearchList({ items }: { items: any[] }) {
  const [searchQuery, setSearchQuery] = useState('')
  const [currentPage, setCurrentPage] = useState(1)
  const itemsPerPage = 100

  const filteredItems = useMemo(() => {
    return items.filter(item =>
      item.name.toLowerCase().includes(searchQuery.toLowerCase())
    )
  }, [items, searchQuery])

  const paginatedItems = useMemo(() => {
    const startIndex = (currentPage - 1) * itemsPerPage
    return filteredItems.slice(startIndex, startIndex + itemsPerPage)
  }, [filteredItems, currentPage])

  const totalPages = Math.ceil(filteredItems.length / itemsPerPage)

  const renderItem = useCallback((item: any, index: number) => (
    <div className="p-2 border-b hover:bg-gray-50">
      {item.name}
    </div>
  ), [])

  return (
    <div className="space-y-4">
      <SearchBar
        placeholder="Search items..."
        onSearch={setSearchQuery}
      />
      
      <VirtualList
        items={paginatedItems}
        itemHeight={40}
        containerHeight={400}
        renderItem={renderItem}
      />
      
      <Pagination
        currentPage={currentPage}
        totalPages={totalPages}
        onPageChange={setCurrentPage}
      />
    </div>
  )
}

Debounced Search with Caching

Implement debounced search with result caching:

import { useState, useCallback, useMemo } from 'react'
import { SearchBar } from '@foundrykit/components'
import { Card, CardContent } from '@foundrykit/primitives'

interface SearchCache {
  [query: string]: {
    results: any[]
    timestamp: number
  }
}

function CachedSearch({ items }: { items: any[] }) {
  const [searchQuery, setSearchQuery] = useState('')
  const [searchResults, setSearchResults] = useState<any[]>([])
  const [isSearching, setIsSearching] = useState(false)
  const [cache, setCache] = useState<SearchCache>({})

  const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes

  const performSearch = useCallback(async (query: string) => {
    if (!query.trim()) {
      setSearchResults([])
      return
    }

    // Check cache first
    const cached = cache[query]
    if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
      setSearchResults(cached.results)
      return
    }

    setIsSearching(true)
    
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 300))
      
      const results = items.filter(item =>
        item.name.toLowerCase().includes(query.toLowerCase())
      )
      
      // Update cache
      setCache(prev => ({
        ...prev,
        [query]: {
          results,
          timestamp: Date.now()
        }
      }))
      
      setSearchResults(results)
    } catch (error) {
      console.error('Search failed:', error)
      setSearchResults([])
    } finally {
      setIsSearching(false)
    }
  }, [items, cache])

  const handleSearch = useCallback((query: string) => {
    setSearchQuery(query)
    performSearch(query)
  }, [performSearch])

  return (
    <Card>
      <CardContent className="space-y-4">
        <SearchBar
          placeholder="Search with caching..."
          onSearch={handleSearch}
        />
        
        {isSearching && (
          <div className="text-sm text-muted-foreground">
            Searching...
          </div>
        )}
        
        <div className="space-y-2">
          {searchResults.map(item => (
            <div key={item.id} className="p-2 border rounded">
              {item.name}
            </div>
          ))}
        </div>
        
        {searchQuery && !isSearching && searchResults.length === 0 && (
          <div className="text-sm text-muted-foreground">
            No results found
          </div>
        )}
      </CardContent>
    </Card>
  )
}

Composition Patterns

Higher-Order Components

Create HOCs to enhance components with common functionality:

import { ComponentType, useState, useCallback } from 'react'
import { SearchBar, Calendar } from '@foundrykit/components'
import { Card, CardContent, CardHeader, CardTitle } from '@foundrykit/primitives'

interface WithSearchProps {
  onSearch: (query: string) => void
  searchPlaceholder?: string
}

function withSearch<T extends object>(
  WrappedComponent: ComponentType<T>
) {
  return function WithSearchComponent(props: T & WithSearchProps) {
    const { onSearch, searchPlaceholder = "Search...", ...rest } = props

    return (
      <div className="space-y-4">
        <SearchBar
          placeholder={searchPlaceholder}
          onSearch={onSearch}
        />
        <WrappedComponent {...(rest as T)} />
      </div>
    )
  }
}

interface WithDateFilterProps {
  onDateChange: (date: Date | undefined) => void
  selectedDate?: Date
}

function withDateFilter<T extends object>(
  WrappedComponent: ComponentType<T>
) {
  return function WithDateFilterComponent(props: T & WithDateFilterProps) {
    const { onDateChange, selectedDate, ...rest } = props

    return (
      <div className="space-y-4">
        <Calendar
          mode="single"
          selected={selectedDate}
          onSelect={onDateChange}
          className="rounded-md border"
        />
        <WrappedComponent {...(rest as T)} />
      </div>
    )
  }
}

// Usage
const DataList = ({ data }: { data: any[] }) => (
  <div className="space-y-2">
    {data.map(item => (
      <div key={item.id} className="p-2 border rounded">
        {item.name}
      </div>
    ))}
  </div>
)

const SearchableDataList = withSearch(DataList)
const DateFilteredDataList = withDateFilter(SearchableDataList)

function EnhancedDataList() {
  const [searchQuery, setSearchQuery] = useState('')
  const [selectedDate, setSelectedDate] = useState<Date>()
  const [data, setData] = useState<any[]>([])

  const handleSearch = useCallback((query: string) => {
    setSearchQuery(query)
    // Filter data based on search query
  }, [])

  const handleDateChange = useCallback((date: Date | undefined) => {
    setSelectedDate(date)
    // Filter data based on date
  }, [])

  return (
    <Card>
      <CardHeader>
        <CardTitle>Enhanced Data List</CardTitle>
      </CardHeader>
      <CardContent>
        <DateFilteredDataList
          data={data}
          onSearch={handleSearch}
          onDateChange={handleDateChange}
          selectedDate={selectedDate}
          searchPlaceholder="Search data..."
        />
      </CardContent>
    </Card>
  )
}

Compound Components

Create compound components for flexible composition:

import { createContext, useContext, useState, ReactNode } from 'react'
import { SearchBar, Calendar, Pagination } from '@foundrykit/components'
import { Card, CardContent, CardHeader, CardTitle } from '@foundrykit/primitives'

interface DataGridContextType {
  searchQuery: string
  selectedDate: Date | undefined
  currentPage: number
  setSearchQuery: (query: string) => void
  setSelectedDate: (date: Date | undefined) => void
  setCurrentPage: (page: number) => void
}

const DataGridContext = createContext<DataGridContextType | undefined>(undefined)

function useDataGrid() {
  const context = useContext(DataGridContext)
  if (!context) {
    throw new Error('DataGrid components must be used within DataGrid')
  }
  return context
}

interface DataGridProps {
  children: ReactNode
}

function DataGrid({ children }: DataGridProps) {
  const [searchQuery, setSearchQuery] = useState('')
  const [selectedDate, setSelectedDate] = useState<Date>()
  const [currentPage, setCurrentPage] = useState(1)

  return (
    <DataGridContext.Provider value={{
      searchQuery,
      selectedDate,
      currentPage,
      setSearchQuery,
      setSelectedDate,
      setCurrentPage
    }}>
      <Card>
        <CardContent className="space-y-4">
          {children}
        </CardContent>
      </Card>
    </DataGridContext.Provider>
  )
}

function DataGridSearch({ placeholder = "Search..." }: { placeholder?: string }) {
  const { searchQuery, setSearchQuery } = useDataGrid()
  
  return (
    <SearchBar
      placeholder={placeholder}
      onSearch={setSearchQuery}
    />
  )
}

function DataGridDateFilter() {
  const { selectedDate, setSelectedDate } = useDataGrid()
  
  return (
    <Calendar
      mode="single"
      selected={selectedDate}
      onSelect={setSelectedDate}
      className="rounded-md border"
    />
  )
}

function DataGridPagination({ totalPages }: { totalPages: number }) {
  const { currentPage, setCurrentPage } = useDataGrid()
  
  return (
    <Pagination
      currentPage={currentPage}
      totalPages={totalPages}
      onPageChange={setCurrentPage}
    />
  )
}

function DataGridContent({ children }: { children: ReactNode }) {
  return <div className="space-y-2">{children}</div>
}

// Usage
function CompoundDataGrid() {
  return (
    <DataGrid>
      <DataGridSearch placeholder="Search items..." />
      <DataGridDateFilter />
      <DataGridContent>
        {/* Your data content here */}
        <div>Data items...</div>
      </DataGridContent>
      <DataGridPagination totalPages={10} />
    </DataGrid>
  )
}

Error Handling Patterns

Error Boundaries with Components

Implement error boundaries for component error handling:

import { Component, ReactNode } from 'react'
import { SearchBar, Calendar } from '@foundrykit/components'
import { Card, CardContent, Button } from '@foundrykit/primitives'

interface ErrorBoundaryState {
  hasError: boolean
  error?: Error
}

interface ErrorBoundaryProps {
  children: ReactNode
  fallback?: ReactNode
}

class ComponentErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, errorInfo: any) {
    console.error('Component error:', error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      if (this.props.fallback) {
        return this.props.fallback
      }

      return (
        <Card>
          <CardContent className="text-center space-y-4">
            <h3 className="text-lg font-semibold">Something went wrong</h3>
            <p className="text-muted-foreground">
              {this.state.error?.message || 'An unexpected error occurred'}
            </p>
            <Button
              onClick={() => this.setState({ hasError: false, error: undefined })}
            >
              Try Again
            </Button>
          </CardContent>
        </Card>
      )
    }

    return this.props.children
  }
}

function SafeComponentWrapper({ children }: { children: ReactNode }) {
  return (
    <ComponentErrorBoundary
      fallback={
        <Card>
          <CardContent className="text-center">
            <p>Component failed to load</p>
          </CardContent>
        </Card>
      }
    >
      {children}
    </ComponentErrorBoundary>
  )
}

function SafeSearchInterface() {
  return (
    <SafeComponentWrapper>
      <div className="space-y-4">
        <SearchBar placeholder="Search..." onSearch={console.log} />
        <Calendar mode="single" />
      </div>
    </SafeComponentWrapper>
  )
}

Next Steps