← Назад к вопросам
Делал ли кастомный функционал для таблиц
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>
);
}
Вызовы при разработке таблиц
- Производительность с большими объемами - решение: виртуализация
- Синхронизация состояния - решение: useMemo, useCallback
- Hydration mismatch в SSR - решение: отложенная гидрация
- Кроссбраузерная совместимость - решение: polyfills
- Доступность (accessibility) - решение: ARIA атрибуты
Да, я регулярно разрабатываю кастомный функционал для таблиц, начиная от простой фильтрации и заканчивая сложными операциями с виртуализацией и экспортом данных.