npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zango-core/crud

v1.0.12

Published

Unified React CRUD components library for tables, forms and workflows.

Downloads

588

Readme

CRUD Library Composable Architecture - Complete Guide

Overview

This document serves as the complete reference for implementing the composable CRUD architecture. The library transforms from a monolithic table component into a fully composable, context-driven system where any combination of default and custom components work seamlessly together.

Core Architecture Philosophy

1. Separation of Concerns

  • Data Logic: Handled by hooks and context (API calls, state management)
  • UI Logic: Handled by components (rendering, user interactions)
  • State Management: Centralized in React Context with perfect synchronization

2. Component Independence with State Sharing

  • Each component can be default or custom
  • All components share the same data/filter/pagination state via Context
  • Perfect synchronization: Filter changes automatically update all views
  • No prop drilling required

3. Composable Building Blocks

  • Mix and match any combination of components
  • Create completely custom layouts while reusing data logic
  • Gradual migration path from existing implementations

Implementation Plan Summary

Phase 1: Core Context & Provider System

  • Create TableContext for centralized state management
  • Implement TableProvider component with all business logic
  • Create useTableContext hook for component consumption

Phase 2: Component Refactoring

  • Refactor existing components to use context instead of props
  • Remove prop drilling from all components
  • Maintain backward compatibility

Phase 3: New Composable Components

  • Create layout components for different use cases
  • Implement specialized hooks for granular control
  • Add render prop components for maximum flexibility

Phase 4: Enhanced Export System

  • Export 50+ items for consumer use
  • Maintain backward compatibility
  • Provide comprehensive TypeScript definitions

Context Structure

Complete TableContext Definition

interface TableContextValue {
  // Data Management
  data: any[];
  isLoading: boolean;
  isFetching: boolean;
  error: Error | null;
  totalCount: number;
  
  // Search State
  searchTerm: string;
  setSearchTerm: (term: string) => void;
  debouncedSearchTerm: string;
  
  // Filter State  
  filterObj: Record<string, any>;
  setFilterObj: (filters: Record<string, any>) => void;
  debouncedFilterObj: Record<string, any>;
  activeFilterCount: number;
  clearFilters: () => void;
  
  // Pagination State
  currentPage: number;
  setCurrentPage: (page: number) => void;
  totalPages: number;
  rowsPerPage: number;
  setRowsPerPage: (size: number) => void;
  
  // Table Instance (for sorting, columns)
  table: Table<any> | null;
  columns: ColumnDef<any>[];
  
  // Selection State
  selectedRows: any[];
  setSelectedRows: (rows: any[]) => void;
  selectRow: (row: any) => void;
  selectAll: () => void;
  clearSelection: () => void;
  
  // View State (for column management)
  columnVisibility: Record<string, boolean>;
  setColumnVisibility: (visibility: Record<string, boolean>) => void;
  columnOrder: string[];
  setColumnOrder: (order: string[]) => void;
  columnPinning: ColumnPinningState;
  setColumnPinning: (pinning: ColumnPinningState) => void;
  sorting: SortingState;
  setSorting: (sorting: SortingState) => void;
  
  // Actions
  refresh: () => void;
  refetch: () => void;
  invalidateCache: () => void;
  
  // Configuration
  apiUrl: string;
  tableId: string;
  config: TableConfig;
  features: TableFeatures;
}

State Synchronization Flow

User Interaction → Context State Update → Data Refetch → All Components Re-render
       ↓
Filter Change → filterObj changes → useTableData hook → All Views Update
Search Change → searchTerm changes → API call → Cards/Table/List Update  
Page Change → currentPage changes → New data fetch → All Components Update

Library Exports (50+ Items)

Context System (4 items)

  • TableProvider - Main provider component
  • useTableContext - Hook to consume context
  • TableContext - Raw context (rarely used directly)
  • TableContextValue - TypeScript interface

Hooks (15 items)

  • useTableData - Main data management hook
  • useTableSearch - Search state and actions
  • useTableFilters - Filter state and actions
  • useTablePagination - Pagination state and actions
  • useTableSelection - Row selection management
  • useTableColumns - Column visibility/ordering
  • useTableSorting - Sorting state management
  • useTableActions - Action methods (refresh, etc.)
  • useTableEvents - Event system integration
  • useTablePersistence - State persistence
  • useTableConfig - Configuration management
  • useTableMetadata - Table metadata access
  • useTableQuery - Direct query access
  • useTableCache - Cache management
  • useTableOptimisticUpdates - Optimistic UI updates

Layout Components (8 items)

  • TableLayout - Default complete layout
  • CompactLayout - Minimal space layout
  • SplitLayout - Side-by-side layout
  • CustomLayout - Render prop based
  • GridLayout - Card/grid optimized layout
  • ListLayout - List view optimized
  • DashboardLayout - Dashboard style
  • MobileLayout - Mobile optimized

UI Components (15 items)

  • TableSearch - Search input component
  • TableFilters - Filter panel component
  • TablePagination - Pagination controls
  • TableUtilities - Action button bar
  • TableHeader - Header section
  • TableViewOptions - Column/view options
  • TableBulkActions - Bulk operation controls
  • TableExportButton - Export functionality
  • TableRefreshButton - Refresh data button
  • TableRowSelector - Row selection checkbox
  • TableSortButton - Column sort control
  • TableFilterChips - Active filter display
  • TableEmptyState - No data state
  • TableLoadingState - Loading indicator
  • TableErrorState - Error handling display

Render Prop Components (5 items)

  • TableData - Data render prop
  • TableControls - Controls render prop
  • TableView - View render prop
  • TableState - State render prop
  • TableActions - Actions render prop

Utilities (10 items)

  • buildApiUrl - URL construction helper
  • formatTableData - Data formatting
  • validateFilters - Filter validation
  • createTableColumns - Column generation
  • handleTableEvents - Event handling
  • serializeTableState - State serialization
  • parseTableConfig - Configuration parsing
  • optimizeTableQuery - Query optimization
  • cacheTableData - Data caching
  • debugTableState - Development debugging

Consumer Usage Examples

Example 1: Default Experience (Backward Compatible)

// Updated to use new CrudHandler
function CustomerPage() {
  return <CrudHandler api_endpoint="/api/customers" />;
}

Example 2: Custom Card View with Default Controls

function CustomerCardPage() {
  return (
    <TableProvider apiUrl="/api/customers">
      {/* Use default search component */}
      <TableSearch />
      
      {/* Use default filter component */}
      <TableFilters />
      
      {/* Custom card view using context data */}
      <CustomCardGrid />
      
      {/* Use default pagination */}
      <TablePagination />
    </TableProvider>
  );
}

function CustomCardGrid() {
  const { data, isLoading } = useTableContext();
  
  if (isLoading) return <div>Loading cards...</div>;
  
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
      {data.map(customer => (
        <CustomerCard key={customer.id} customer={customer} />
      ))}
    </div>
  );
}

Example 3: Custom Layout with Mixed Components

function ProductDashboard() {
  return (
    <TableProvider apiUrl="/api/products">
      <div className="dashboard-layout">
        {/* Custom sidebar with default filter logic */}
        <aside className="sidebar">
          <h2>Filters</h2>
          <TableFilters />
        </aside>
        
        <main className="content">
          {/* Custom header */}
          <header className="flex justify-between items-center mb-4">
            <h1>Products</h1>
            <div className="flex gap-2">
              <TableSearch />
              <TableUtilities />
            </div>
          </header>
          
          {/* Custom kanban view */}
          <ProductKanbanBoard />
          
          {/* Default pagination */}
          <TablePagination />
        </main>
      </div>
    </TableProvider>
  );
}

Example 4: Completely Custom with Hooks

function CustomProductManager() {
  const {
    data,
    isLoading,
    searchTerm,
    setSearchTerm,
    filterObj,
    setFilterObj,
    currentPage,
    setCurrentPage,
    totalPages,
    refresh
  } = useTableData({ apiUrl: '/api/products' });
  
  return (
    <div className="custom-manager">
      {/* Completely custom search */}
      <div className="search-section">
        <input
          type="text"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="Search products..."
          className="custom-search"
        />
      </div>
      
      {/* Custom filter buttons */}
      <div className="filter-buttons">
        <button 
          onClick={() => setFilterObj({...filterObj, category: 'electronics'})}
          className={filterObj.category === 'electronics' ? 'active' : ''}
        >
          Electronics
        </button>
        <button 
          onClick={() => setFilterObj({...filterObj, category: 'clothing'})}
          className={filterObj.category === 'clothing' ? 'active' : ''}
        >
          Clothing
        </button>
      </div>
      
      {/* Custom grid view */}
      <div className="product-grid">
        {data.map(product => (
          <CustomProductCard key={product.id} product={product} />
        ))}
      </div>
      
      {/* Custom pagination */}
      <div className="pagination">
        <button 
          disabled={currentPage === 1}
          onClick={() => setCurrentPage(currentPage - 1)}
        >
          Previous
        </button>
        <span>Page {currentPage} of {totalPages}</span>
        <button 
          disabled={currentPage === totalPages}
          onClick={() => setCurrentPage(currentPage + 1)}
        >
          Next
        </button>
      </div>
    </div>
  );
}

Example 5: Render Props Pattern

function OrderManagement() {
  return (
    <TableData apiUrl="/api/orders">
      {({ data, isLoading, filters, pagination, actions }) => (
        <div className="order-management">
          {/* Custom controls using render prop data */}
          <OrderControls 
            filters={filters}
            onRefresh={actions.refresh}
          />
          
          {/* Custom view using data */}
          <OrderTimeline 
            orders={data}
            loading={isLoading}
          />
          
          {/* Custom pagination using pagination data */}
          <OrderPagination 
            current={pagination.currentPage}
            total={pagination.totalPages}
            onChange={pagination.setCurrentPage}
          />
        </div>
      )}
    </TableData>
  );
}

Example 6: Advanced Custom Filter Integration

function AdvancedCustomerFilters() {
  const { filterObj, setFilterObj, activeFilterCount, clearFilters } = useTableFilters();
  
  const handleLocationFilter = (location) => {
    setFilterObj({
      ...filterObj,
      location: location
    });
  };
  
  const handleDateRangeFilter = (startDate, endDate) => {
    setFilterObj({
      ...filterObj,
      dateRange: [startDate, endDate]
    });
  };
  
  return (
    <div className="advanced-filters">
      {/* Custom location filter */}
      <div className="filter-group">
        <label>Location</label>
        <select 
          value={filterObj.location || ''}
          onChange={(e) => handleLocationFilter(e.target.value)}
        >
          <option value="">All Locations</option>
          <option value="north">North Region</option>
          <option value="south">South Region</option>
        </select>
      </div>
      
      {/* Custom date range filter */}
      <div className="filter-group">
        <label>Date Range</label>
        <CustomDateRangePicker
          onDateChange={handleDateRangeFilter}
          value={filterObj.dateRange}
        />
      </div>
      
      {/* Filter status and clear */}
      <div className="filter-status">
        {activeFilterCount > 0 && (
          <>
            <span>{activeFilterCount} filters active</span>
            <button onClick={clearFilters}>Clear All</button>
          </>
        )}
      </div>
    </div>
  );
}

Example 7: Mobile-Optimized Layout

function MobileCustomerList() {
  return (
    <TableProvider apiUrl="/api/customers">
      <MobileLayout>
        {/* Mobile-friendly search */}
        <div className="mobile-header">
          <TableSearch placeholder="Search customers..." />
        </div>
        
        {/* Collapsible filters */}
        <MobileFilterDrawer>
          <TableFilters />
        </MobileFilterDrawer>
        
        {/* List view for mobile */}
        <CustomMobileList />
        
        {/* Infinite scroll pagination */}
        <InfiniteScrollPagination />
      </MobileLayout>
    </TableProvider>
  );
}

Example 8: Split View Layout

function CustomerDetailSplit() {
  const [selectedCustomer, setSelectedCustomer] = useState(null);
  
  return (
    <TableProvider apiUrl="/api/customers">
      <div className="split-layout">
        {/* Left panel - List */}
        <div className="left-panel">
          <TableSearch />
          <CustomCustomerList 
            onCustomerSelect={setSelectedCustomer}
            selectedId={selectedCustomer?.id}
          />
          <TablePagination />
        </div>
        
        {/* Right panel - Detail */}
        <div className="right-panel">
          {selectedCustomer ? (
            <CustomerDetail customer={selectedCustomer} />
          ) : (
            <div>Select a customer to view details</div>
          )}
        </div>
      </div>
    </TableProvider>
  );
}

Implementation Files to Create

Core System Files

src/table/context/
├── TableContext.tsx           # Context definition
├── TableProvider.tsx          # Provider with all logic
├── useTableContext.tsx        # Context consumer hook
└── index.ts                   # Context exports

src/table/hooks/
├── useTableData.tsx           # Main data hook
├── useTableSearch.tsx         # Search management
├── useTableFilters.tsx        # Filter management  
├── useTablePagination.tsx     # Pagination management
├── useTableSelection.tsx      # Selection management
└── [10 more specialized hooks]

src/table/layouts/
├── TableLayout.tsx            # Default layout
├── CompactLayout.tsx          # Space-efficient
├── SplitLayout.tsx            # Side-by-side
├── CustomLayout.tsx           # Render props
└── [4 more layout variants]

src/table/render-props/
├── TableData.tsx              # Data render prop
├── TableControls.tsx          # Controls render prop
├── TableView.tsx              # View render prop
└── [2 more render prop components]

Migration Strategy

Phase 1: Maintain Backward Compatibility

  • Existing <Table apiUrl={url} /> is replaced by <CrudHandler api_endpoint={url} />
  • No breaking changes for current consumers
  • Internal refactoring only

Phase 2: Introduce New APIs

  • Add <TableProvider> pattern alongside existing API
  • Provide new hooks for custom development
  • Document migration benefits

Phase 3: Full Composable Ecosystem

  • Complete library of composable components
  • Comprehensive examples and documentation
  • Performance optimizations

Key Benefits

  1. Perfect State Synchronization: All components stay in sync automatically
  2. Component Agnostic: Mix default and custom components freely
  3. Gradual Migration: Adopt new patterns incrementally
  4. Type Safety: Full TypeScript support throughout
  5. Performance: Optimized re-rendering and API calls
  6. Developer Experience: Intuitive APIs with comprehensive examples

Development Timeline

  • Week 1: Context system and provider setup
  • Week 2: Refactor existing components
  • Week 3: New hooks and layout components
  • Week 4: Render props and optimization
  • Week 5: Documentation and examples

This architecture enables infinite customization possibilities while maintaining the simplicity of the original API for basic use cases.


Array Field Row-Level Controls

The form system now supports row-level button control for array fields, allowing you to control the visibility of action buttons (remove, copy, move up/down) and set read-only state on individual rows within array fields.

Features

  • Per-row button visibility: Control which action buttons appear for each row
  • Read-only rows: Make specific rows non-editable while keeping others editable
  • Backward compatibility: Existing forms continue working unchanged
  • Fallback support: Row-level flags take precedence over field-level ui:options

Row-Level Flags

Add these flags to individual row data in your array field defaults:

| Flag | Type | Description | |------|------|-------------| | _hasRemove | boolean | Show/hide the remove (trash) button for this row | | _hasCopy | boolean | Show/hide the copy button for this row | | _hasMoveUp | boolean | Show/hide the move up (arrow up) button for this row | | _hasMoveDown | boolean | Show/hide the move down (arrow down) button for this row | | _isReadOnly | boolean | Make the entire row read-only (disables all form fields and buttons) |

Usage Examples

Basic Example: Hide Remove Button

{
  "functional_team_members": {
    "type": "array",
    "default": [
      {
        "department": "local_safety",
        "name": "Rajat",
        "email": "[email protected]",
        "_hasRemove": false  // This row cannot be removed
      },
      {
        "department": "commercial_marketing",
        // No flags = uses field-level settings or defaults
      }
    ]
  }
}

Advanced Example: Mixed Row States

{
  "business_team_members": {
    "type": "array",
    "title": "Business Team Members",
    "default": [
      {
        // Read-only system user - cannot be edited or removed
        "department": "system_admin",
        "name": "System Administrator",
        "email": "[email protected]",
        "_isReadOnly": true,
        "_hasRemove": false,
        "_hasCopy": false,
        "_hasMoveUp": false,
        "_hasMoveDown": false
      },
      {
        // Required user - can be edited but not removed
        "department": "business_reviewer",
        "name": "John Doe",
        "email": "[email protected]",
        "_hasRemove": false,
        "_hasCopy": true  // Allow copying this template
      },
      {
        // Standard editable row with all controls
        "department": "program_owner"
        // Uses default field-level settings
      }
    ]
  }
}

Complete Form Schema Example

{
  "json_schema": {
    "type": "object",
    "properties": {
      "team_members": {
        "type": "array",
        "title": "Team Members",
        "minItems": 1,
        "items": {
          "type": "object",
          "required": ["name", "email", "role"],
          "properties": {
            "name": {"type": "string", "title": "Name"},
            "email": {"type": "string", "title": "Email", "format": "email"},
            "role": {
              "type": "string",
              "title": "Role",
              "enum": ["admin", "user", "viewer"]
            }
          }
        },
        "default": [
          {
            "name": "System Admin",
            "email": "[email protected]",
            "role": "admin",
            "_isReadOnly": true,
            "_hasRemove": false
          },
          {
            "name": "Template User",
            "email": "[email protected]",
            "role": "user",
            "_hasCopy": true,
            "_hasRemove": false
          }
        ]
      }
    }
  },
  "ui_schema": {
    "team_members": {
      "ui:options": {
        "orderable": true,   // Field-level default: allow reordering
        "addable": true,     // Field-level default: allow adding
        "removable": true    // Field-level default: allow removing
      }
    }
  }
}

Precedence Rules

The system follows this precedence order:

  1. Row-level flags (highest priority)
  2. Field-level ui:options (fallback)
  3. System defaults (lowest priority)
// Row-level flag overrides field-level setting
{
  "ui_schema": {
    "members": {
      "ui:options": {
        "removable": true  // Field says "allow remove"
      }
    }
  },
  "default": [
    {
      "name": "John",
      "_hasRemove": false  // Row-level override: this row cannot be removed
    }
  ]
}

Read-Only Row Behavior

When _isReadOnly: true is set on a row:

  • Visual changes: Row becomes slightly transparent (opacity-75)
  • Form fields disabled: All input fields become non-interactive
  • Buttons disabled: All action buttons are disabled and grayed out
  • Pointer events disabled: No mouse interactions allowed on form fields
{
  "default": [
    {
      "name": "Protected User",
      "email": "[email protected]",
      "_isReadOnly": true  // This entire row cannot be edited
    }
  ]
}

Data Cleaning

The system automatically removes all internal flags before form submission:

  • _hasRemove, _hasCopy, _hasMoveUp, _hasMoveDown, _isReadOnly are stripped
  • Clean data is sent to both custom on_submit handlers and API endpoints
  • No manual cleaning required

Migration Notes

  • Existing forms: Continue working unchanged
  • No breaking changes: All current functionality preserved
  • Gradual adoption: Add row-level flags only where needed
  • Field-level fallback: Existing ui:options still work as before

Implementation Details

The feature uses:

  • Lucide React icons: ArrowUp, ArrowDown, Copy, Trash
  • Utility functions: Automatic flag extraction and data cleaning
  • Context preservation: Maintains existing RJSF architecture
  • Performance optimized: Minimal re-rendering impact