Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Создание таблиц в React: подходы и лучшие практики
Таблицы — важный компонент UI, требующий внимания к доступности, производительности и удобству использования. Рассмотрю, как я организую работу с таблицами в реальных проектах.
1. Базовая структура таблицы в React
// Сначала определяем типы данных
interface TableData {
id: string;
name: string;
email: string;
status: "active" | "inactive";
createdAt: string;
}
// Компонент таблицы
interface TableProps {
data: TableData[];
isLoading?: boolean;
onRowClick?: (row: TableData) => void;
}
export function DataTable({ data, isLoading, onRowClick }: TableProps) {
if (isLoading) return <div>Loading...</div>;
return (
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-surface-secondary border-b border-border-default">
<th className="px-4 py-2 text-left text-content-primary">Name</th>
<th className="px-4 py-2 text-left text-content-primary">Email</th>
<th className="px-4 py-2 text-left text-content-primary">Status</th>
<th className="px-4 py-2 text-left text-content-primary">Created</th>
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr
key={row.id}
className="border-b border-border-default hover:bg-surface-secondary cursor-pointer transition-colors"
onClick={() => onRowClick?.(row)}
>
<td className="px-4 py-2 text-content-primary">{row.name}</td>
<td className="px-4 py-2 text-content-secondary">{row.email}</td>
<td className="px-4 py-2">
<span className={getStatusBadgeClass(row.status)}>
{row.status}
</span>
</td>
<td className="px-4 py-2 text-content-secondary">
{formatDate(row.createdAt)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function getStatusBadgeClass(status: "active" | "inactive") {
return `px-2 py-1 rounded-md text-sm font-medium ${
status === "active"
? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800"
}`;
}
2. Таблица с сортировкой и фильтрацией
// hooks/useTableState.ts
import { useState, useCallback, useMemo } from "react";
interface UseTableStateProps<T> {
data: T[];
defaultSort?: { key: keyof T; order: "asc" | "desc" };
}
export function useTableState<T>({ data, defaultSort }: UseTableStateProps<T>) {
const [sortKey, setSortKey] = useState<keyof T | null>(defaultSort?.key ?? null);
const [sortOrder, setSortOrder] = useState<"asc" | "desc">(defaultSort?.order ?? "asc");
const [filters, setFilters] = useState<Record<string, string>>({});
const [searchTerm, setSearchTerm] = useState("");
// Сортировка
const handleSort = useCallback((key: keyof T) => {
setSortKey(key);
setSortOrder(prev => (prev === "asc" ? "desc" : "asc"));
}, []);
// Фильтрация
const filteredData = useMemo(() => {
return data.filter(item => {
// Фильтр по поисковому запросу
if (searchTerm) {
const searchStr = JSON.stringify(item).toLowerCase();
if (!searchStr.includes(searchTerm.toLowerCase())) return false;
}
// Фильтры по полям
return Object.entries(filters).every(([key, value]) => {
if (!value) return true;
return String((item as any)[key]).toLowerCase().includes(value.toLowerCase());
});
});
}, [data, filters, searchTerm]);
// Сортировка
const sortedData = useMemo(() => {
if (!sortKey) return filteredData;
return [...filteredData].sort((a, b) => {
const aVal = a[sortKey];
const bVal = b[sortKey];
if (aVal < bVal) return sortOrder === "asc" ? -1 : 1;
if (aVal > bVal) return sortOrder === "asc" ? 1 : -1;
return 0;
});
}, [filteredData, sortKey, sortOrder]);
return {
data: sortedData,
sortKey,
sortOrder,
handleSort,
filters,
setFilters,
searchTerm,
setSearchTerm,
};
}
// Использование в компоненте
function UserTable({ users }: { users: TableData[] }) {
const { data, sortKey, sortOrder, handleSort, searchTerm, setSearchTerm } =
useTableState({ data: users, defaultSort: { key: "name", order: "asc" } });
return (
<div>
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="mb-4 px-4 py-2 border border-border-default rounded-lg"
/>
<table className="w-full">
<thead>
<tr>
<th
className="cursor-pointer hover:bg-surface-secondary"
onClick={() => handleSort("name")}
>
Name {sortKey === "name" && (sortOrder === "asc" ? "↑" : "↓")}
</th>
<th className="cursor-pointer" onClick={() => handleSort("status")}>
Status {sortKey === "status" && (sortOrder === "asc" ? "↑" : "↓")}
</th>
</tr>
</thead>
<tbody>
{data.map(row => (
<tr key={row.id}>
<td>{row.name}</td>
<td>{row.status}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
3. Таблица с пагинацией
// hooks/usePagination.ts
export function usePagination<T>(data: T[], pageSize: number = 10) {
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(data.length / pageSize);
const startIndex = (currentPage - 1) * pageSize;
const paginatedData = data.slice(startIndex, startIndex + pageSize);
const goToPage = useCallback((page: number) => {
const validPage = Math.max(1, Math.min(page, totalPages));
setCurrentPage(validPage);
}, [totalPages]);
return {
data: paginatedData,
currentPage,
totalPages,
goToPage,
hasNextPage: currentPage < totalPages,
hasPrevPage: currentPage > 1,
};
}
// Использование
function PaginatedTable({ users }: { users: TableData[] }) {
const { data, currentPage, totalPages, goToPage, hasNextPage, hasPrevPage } =
usePagination(users, 20);
return (
<div>
<DataTable data={data} />
<div className="flex items-center gap-2 mt-4">
<button
onClick={() => goToPage(currentPage - 1)}
disabled={!hasPrevPage}
className="px-4 py-2 border border-border-default rounded-lg disabled:opacity-50"
>
Previous
</button>
<span className="text-content-secondary">
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => goToPage(currentPage + 1)}
disabled={!hasNextPage}
className="px-4 py-2 border border-border-default rounded-lg disabled:opacity-50"
>
Next
</button>
</div>
</div>
);
}
4. Таблица с выбором строк (checkboxes)
// hooks/useTableSelection.ts
export function useTableSelection<T extends { id: string }>(data: T[]) {
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const toggleRow = useCallback((id: string) => {
setSelectedIds(prev => {
const newSet = new Set(prev);
if (newSet.has(id)) {
newSet.delete(id);
} else {
newSet.add(id);
}
return newSet;
});
}, []);
const toggleAll = useCallback(() => {
if (selectedIds.size === data.length) {
setSelectedIds(new Set());
} else {
setSelectedIds(new Set(data.map(item => item.id)));
}
}, [data, selectedIds.size]);
const isSelected = useCallback((id: string) => selectedIds.has(id), [selectedIds]);
return {
selectedIds,
toggleRow,
toggleAll,
isSelected,
selectedCount: selectedIds.size,
};
}
// Использование в компоненте
function SelectableTable({ users }: { users: TableData[] }) {
const { toggleRow, toggleAll, isSelected, selectedCount } = useTableSelection(users);
return (
<div>
{selectedCount > 0 && (
<div className="mb-4 p-3 bg-surface-secondary rounded-lg">
{selectedCount} selected - <button>Delete</button>
</div>
)}
<table className="w-full">
<thead>
<tr>
<th>
<input
type="checkbox"
onChange={toggleAll}
checked={selectedCount === users.length && users.length > 0}
/>
</th>
<th>Name</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>
<input
type="checkbox"
checked={isSelected(user.id)}
onChange={() => toggleRow(user.id)}
/>
</td>
<td>{user.name}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
5. Доступность таблиц (a11y)
// Правильная разметка для скринридеров
export function AccessibleTable({ data }: { data: TableData[] }) {
return (
<div className="overflow-x-auto" role="region" aria-label="User data table">
<table role="table" className="w-full">
<caption className="sr-only">List of users with their details</caption>
<thead>
<tr role="row">
<th role="columnheader" scope="col">Name</th>
<th role="columnheader" scope="col">Email</th>
<th role="columnheader" scope="col" aria-sort="ascending">Status</th>
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr key={row.id} role="row">
<td role="cell">{row.name}</td>
<td role="cell">{row.email}</td>
<td role="cell" aria-label={`Status: ${row.status}`}>
{row.status}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Рекомендация
Для таблиц используй:
- Типизированные данные (TypeScript)
- Кастомные хуки (usePagination, useTableState, useTableSelection)
- Композиция компонентов (отделённые TableHeader, TableRow, TableCell)
- Доступность (role, aria-label, scope)
- Производительность (useMemo для больших наборов данных)