← Назад к вопросам

Делал ли кастомный функционал для таблиц

2.3 Middle🔥 131 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Делал ли кастомный функционал для таблиц

Да, я активно разрабатываю кастомные функции для таблиц, так как это критичный элемент для работы с данными. Стандартные библиотеки часто не подходят для специфических требований, поэтому нужны нестандартные решения.

Примеры кастомного функционала

1. Продвинутая фильтрация и поиск

// components/table/TableFilter.tsx

interface FilterConfig {
  field: string;
  operator: 'equals' | 'contains' | 'startsWith' | 'range' | 'dateRange';
  value: any;
}

function useTableFilter(data: any[], filters: FilterConfig[]) {
  return useMemo(() => {
    return data.filter(item => {
      return filters.every(filter => {
        const fieldValue = item[filter.field];
        
        switch (filter.operator) {
          case 'equals':
            return fieldValue === filter.value;
          
          case 'contains':
            return String(fieldValue).toLowerCase()
              .includes(String(filter.value).toLowerCase());
          
          case 'startsWith':
            return String(fieldValue).startsWith(filter.value);
          
          case 'range':
            return fieldValue >= filter.value[0] && fieldValue <= filter.value[1];
          
          case 'dateRange':
            const date = new Date(fieldValue);
            const [start, end] = filter.value;
            return date >= start && date <= end;
          
          default:
            return true;
        }
      });
    });
  }, [data, filters]);
}

2. Сортировка по нескольким колонкам

// Сортировка с поддержкой множественных колонок

interface SortConfig {
  field: string;
  direction: 'asc' | 'desc';
  priority: number; // какая колонка сортируется первой
}

function useTableSort(data: any[], sorts: SortConfig[]) {
  return useMemo(() => {
    const sorted = [...data];
    
    // Сортируем по приоритету
    const sortedConfigs = [...sorts].sort((a, b) => a.priority - b.priority);
    
    sorted.sort((a, b) => {
      for (const config of sortedConfigs) {
        const aVal = a[config.field];
        const bVal = b[config.field];
        
        if (aVal === bVal) continue;
        
        const comparison = aVal < bVal ? -1 : 1;
        return config.direction === 'asc' ? comparison : -comparison;
      }
      
      return 0;
    });
    
    return sorted;
  }, [data, sorts]);
}

3. Виртуализация для больших таблиц

// Отрисовка только видимых строк для производительности

import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';

function VirtualizedTable({ data, columns }: Props) {
  const Row = ({ index, style }: any) => (
    <div style={style} className="table-row">
      {columns.map(col => (
        <div key={col.key} className="table-cell">
          {data[index][col.key]}
        </div>
      ))}
    </div>
  );
  
  return (
    <AutoSizer>
      {({ height, width }) => (
        <FixedSizeList
          height={height}
          itemCount={data.length}
          itemSize={40}
          width={width}
        >
          {Row}
        </FixedSizeList>
      )}
    </AutoSizer>
  );
}

4. Встроенное редактирование ячеек

interface CellEditorProps {
  value: any;
  onChange: (newValue: any) => void;
  type: 'text' | 'number' | 'select' | 'date';
  options?: any[];
}

function EditableCell({ value, onChange, type, options }: CellEditorProps) {
  const [isEditing, setIsEditing] = useState(false);
  const [tempValue, setTempValue] = useState(value);
  
  const handleSave = () => {
    onChange(tempValue);
    setIsEditing(false);
  };
  
  if (!isEditing) {
    return (
      <div 
        className="cell editable" 
        onClick={() => setIsEditing(true)}
      >
        {value}
      </div>
    );
  }
  
  return (
    <div className="cell-editor">
      {type === 'text' && (
        <input
          type="text"
          value={tempValue}
          onChange={(e) => setTempValue(e.target.value)}
          onBlur={handleSave}
          onKeyDown={(e) => e.key === 'Enter' && handleSave()}
          autoFocus
        />
      )}
      
      {type === 'select' && (
        <select
          value={tempValue}
          onChange={(e) => setTempValue(e.target.value)}
          onBlur={handleSave}
          autoFocus
        >
          {options?.map(opt => (
            <option key={opt.id} value={opt.id}>{opt.label}</option>
          ))}
        </select>
      )}
      
      {/* Другие типы... */}
    </div>
  );
}

5. Выделение и массовые операции

function useTableSelection(data: any[]) {
  const [selected, setSelected] = useState<Set<string>>(new Set());
  
  const toggleRow = (id: string) => {
    const newSelected = new Set(selected);
    if (newSelected.has(id)) {
      newSelected.delete(id);
    } else {
      newSelected.add(id);
    }
    setSelected(newSelected);
  };
  
  const toggleAll = () => {
    if (selected.size === data.length) {
      setSelected(new Set());
    } else {
      setSelected(new Set(data.map(item => item.id)));
    }
  };
  
  const getSelectedData = () => {
    return data.filter(item => selected.has(item.id));
  };
  
  const deleteSelected = async () => {
    const selectedIds = Array.from(selected);
    await fetch('/api/bulk-delete', {
      method: 'POST',
      body: JSON.stringify({ ids: selectedIds })
    });
    setSelected(new Set());
  };
  
  return {
    selected,
    toggleRow,
    toggleAll,
    getSelectedData,
    deleteSelected
  };
}

6. Экспорт данных

function exportTableData(data: any[], columns: Column[], format: 'csv' | 'excel' | 'pdf') {
  if (format === 'csv') {
    exportAsCSV(data, columns);
  } else if (format === 'excel') {
    exportAsExcel(data, columns);
  } else if (format === 'pdf') {
    exportAsPDF(data, columns);
  }
}

function exportAsCSV(data: any[], columns: Column[]) {
  // Заголовки
  const headers = columns.map(col => col.label).join(',');
  
  // Строки
  const rows = data.map(item =>
    columns.map(col => {
      const value = item[col.key];
      // Экранировать кавычки
      return `"${String(value).replace(/"/g, '""')}"`;
    }).join(',')
  );
  
  const csv = [headers, ...rows].join('\n');
  
  // Скачать
  const blob = new Blob([csv], { type: 'text/csv' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `export_${new Date().toISOString()}.csv`;
  a.click();
  URL.revokeObjectURL(url);
}

7. Фиксированные колонки (sticky columns)

function TableWithStickyColumns({ data, columns }: Props) {
  const stickyColumns = columns.filter(c => c.sticky);
  const normalColumns = columns.filter(c => !c.sticky);
  
  return (
    <div className="table-container">
      <div className="table-sticky">
        {/* Фиксированные колонки слева */}
        {stickyColumns.map(col => (
          <div key={col.key} className="column sticky-column">
            <div className="column-header">{col.label}</div>
            {data.map(row => (
              <div key={row.id} className="column-cell">
                {row[col.key]}
              </div>
            ))}
          </div>
        ))}
      </div>
      
      <div className="table-scroll">
        {/* Прокручиваемые колонки */}
        {normalColumns.map(col => (
          <div key={col.key} className="column">
            <div className="column-header">{col.label}</div>
            {data.map(row => (
              <div key={row.id} className="column-cell">
                {row[col.key]}
              </div>
            ))}
          </div>
        ))}
      </div>
    </div>
  );
}

8. Группировка и вложенные таблицы

interface GroupedData {
  groupKey: string;
  items: any[];
}

function groupTableData(data: any[], groupBy: string): GroupedData[] {
  const grouped = new Map<string, any[]>();
  
  data.forEach(item => {
    const key = item[groupBy];
    if (!grouped.has(key)) {
      grouped.set(key, []);
    }
    grouped.get(key)!.push(item);
  });
  
  return Array.from(grouped.entries()).map(([groupKey, items]) => ({
    groupKey,
    items
  }));
}

function GroupedTable({ data, groupBy }: Props) {
  const grouped = useMemo(() => groupTableData(data, groupBy), [data, groupBy]);
  
  return (
    <div className="grouped-table">
      {grouped.map(group => (
        <div key={group.groupKey} className="group">
          <div className="group-header">
            {groupBy}: {group.groupKey}
            <span className="count">({group.items.length})</span>
          </div>
          <div className="group-items">
            {group.items.map(item => (
              <div key={item.id} className="group-item">
                {/* Отрисовка элемента */}
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

9. Пагинация с кэшированием

function useTablePagination(data: any[], pageSize: number = 20) {
  const [page, setPage] = useState(1);
  
  const paginatedData = useMemo(() => {
    const start = (page - 1) * pageSize;
    return data.slice(start, start + pageSize);
  }, [data, page, pageSize]);
  
  const totalPages = Math.ceil(data.length / pageSize);
  
  return {
    data: paginatedData,
    page,
    setPage,
    totalPages,
    hasNextPage: page < totalPages,
    hasPrevPage: page > 1
  };
}

Реальный пример: Таблица вопросов интервью

В моем проекте PrepBro я реализовал таблицу с:

interface Question {
  id: string;
  title: string;
  category: string;
  difficulty: 'easy' | 'medium' | 'hard';
  answerCount: number;
  rating: number;
}

function QuestionsTable() {
  const [data, setData] = useState<Question[]>([]);
  
  // Фильтрация
  const [filters, setFilters] = useState<FilterConfig[]>([]);
  const filtered = useTableFilter(data, filters);
  
  // Сортировка
  const [sorts, setSorts] = useState<SortConfig[]>([]);
  const sorted = useTableSort(filtered, sorts);
  
  // Выделение
  const selection = useTableSelection(sorted);
  
  // Пагинация
  const pagination = useTablePagination(sorted);
  
  const handleBulkDelete = async () => {
    if (confirm(`Удалить ${selection.selected.size} вопросов?`)) {
      await selection.deleteSelected();
      setData(prev => 
        prev.filter(item => !selection.selected.has(item.id))
      );
    }
  };
  
  const handleExport = () => {
    const selectedData = selection.getSelectedData();
    exportTableData(selectedData, COLUMNS, 'csv');
  };
  
  return (
    <div className="questions-table">
      <div className="table-toolbar">
        <FilterPanel onChange={setFilters} />
        <div className="actions">
          <button onClick={handleExport} disabled={selection.selected.size === 0}>
            Экспорт ({selection.selected.size})
          </button>
          <button onClick={handleBulkDelete} disabled={selection.selected.size === 0}>
            Удалить
          </button>
        </div>
      </div>
      
      <VirtualizedTable
        data={pagination.data}
        columns={COLUMNS}
        onSort={setSorts}
        selected={selection.selected}
        onSelectRow={selection.toggleRow}
        onSelectAll={selection.toggleAll}
      />
      
      <Pagination {...pagination} />
    </div>
  );
}

Вызовы при разработке таблиц

  1. Производительность с большими объемами - решение: виртуализация
  2. Синхронизация состояния - решение: useMemo, useCallback
  3. Hydration mismatch в SSR - решение: отложенная гидрация
  4. Кроссбраузерная совместимость - решение: polyfills
  5. Доступность (accessibility) - решение: ARIA атрибуты

Да, я регулярно разрабатываю кастомный функционал для таблиц, начиная от простой фильтрации и заканчивая сложными операциями с виртуализацией и экспортом данных.