← Назад к вопросам
Создать React-компонент таблицы с сортировкой
2.0 Middle🔥 181 комментариев
#React
Условие
Создайте React-компонент таблицы с возможностью сортировки по столбцам.
Требования
- Компонент принимает массив данных и конфигурацию столбцов
- При клике на заголовок столбца - сортировка по возрастанию
- При повторном клике - сортировка по убыванию
- Показывать индикатор направления сортировки
- Использовать функциональные компоненты и хуки
Пример использования
const data = [
{ id: 1, name: "Alice", age: 25 },
{ id: 2, name: "Bob", age: 30 },
{ id: 3, name: "Charlie", age: 20 }
];
const columns = [
{ key: "name", label: "Name" },
{ key: "age", label: "Age" }
];
<SortableTable data={data} columns={columns} />
Бонус
Добавьте пагинацию с настраиваемым количеством элементов на странице.
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Задача на React-компонент таблицы с сортировкой — показывает понимание хуков, состояния и работы с данными. Создадим несколько версий.
Решение 1: Базовая таблица с сортировкой
import React, { useState, useMemo } from "react";
interface Column<T> {
key: keyof T;
label: string;
}
interface SortableTableProps<T extends Record<string, any>> {
data: T[];
columns: Column<T>[];
}
type SortOrder = "asc" | "desc" | null;
function SortableTable<T extends Record<string, any>>({
data,
columns,
}: SortableTableProps<T>) {
const [sortKey, setSortKey] = useState<keyof T | null>(null);
const [sortOrder, setSortOrder] = useState<SortOrder>(null);
// Обработчик клика по заголовку
const handleSort = (key: keyof T) => {
if (sortKey === key) {
// Если кликнули на тот же столбец
if (sortOrder === "asc") {
setSortOrder("desc"); // asc -> desc
} else if (sortOrder === "desc") {
setSortKey(null); // desc -> no sort
setSortOrder(null);
}
} else {
// Новый столбец для сортировки
setSortKey(key);
setSortOrder("asc");
}
};
// Сортировка данных
const sortedData = useMemo(() => {
if (!sortKey || !sortOrder) return data;
return [...data].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;
});
}, [data, sortKey, sortOrder]);
return (
<table className="table">
<thead>
<tr>
{columns.map((column) => (
<th
key={String(column.key)}
onClick={() => handleSort(column.key)}
className="cursor-pointer hover:bg-gray-100 px-4 py-2"
>
<div className="flex items-center gap-2">
{column.label}
{sortKey === column.key && (
<span className="text-sm">
{sortOrder === "asc" ? "↑" : "↓"}
</span>
)}
</div>
</th>
))}
</tr>
</thead>
<tbody>
{sortedData.map((row, idx) => (
<tr key={idx} className="border-t hover:bg-gray-50">
{columns.map((column) => (
<td key={String(column.key)} className="px-4 py-2">
{String(row[column.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
export default SortableTable;
Анализ:
useState— для сохранения состояния сортировкиuseMemo— оптимизация, пересчитываем только при изменении данных- Типизация через generics
- Индикатор направления (↑/↓)
Решение 2: С пагинацией (бонус)
interface SortableTableWithPaginationProps<T extends Record<string, any>> {
data: T[];
columns: Column<T>[];
itemsPerPage?: number;
}
function SortableTableWithPagination<T extends Record<string, any>>({
data,
columns,
itemsPerPage = 10,
}: SortableTableWithPaginationProps<T>) {
const [sortKey, setSortKey] = useState<keyof T | null>(null);
const [sortOrder, setSortOrder] = useState<SortOrder>(null);
const [currentPage, setCurrentPage] = useState(1);
const handleSort = (key: keyof T) => {
if (sortKey === key) {
if (sortOrder === "asc") {
setSortOrder("desc");
} else if (sortOrder === "desc") {
setSortKey(null);
setSortOrder(null);
}
} else {
setSortKey(key);
setSortOrder("asc");
}
setCurrentPage(1); // Сброс на первую страницу
};
// Сортировка
const sortedData = useMemo(() => {
if (!sortKey || !sortOrder) return data;
return [...data].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;
});
}, [data, sortKey, sortOrder]);
// Пагинация
const totalPages = Math.ceil(sortedData.length / itemsPerPage);
const paginatedData = useMemo(() => {
const start = (currentPage - 1) * itemsPerPage;
return sortedData.slice(start, start + itemsPerPage);
}, [sortedData, currentPage, itemsPerPage]);
return (
<div>
<table className="table w-full">
<thead>
<tr className="bg-gray-100 border-b">
{columns.map((column) => (
<th
key={String(column.key)}
onClick={() => handleSort(column.key)}
className="cursor-pointer hover:bg-gray-200 px-4 py-3 text-left font-semibold"
>
<div className="flex items-center gap-2">
{column.label}
{sortKey === column.key && (
<span className="text-sm">
{sortOrder === "asc" ? "↑" : "↓"}
</span>
)}
</div>
</th>
))}
</tr>
</thead>
<tbody>
{paginatedData.map((row, idx) => (
<tr key={idx} className="border-b hover:bg-gray-50">
{columns.map((column) => (
<td key={String(column.key)} className="px-4 py-3">
{String(row[column.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
{/* Пагинация */}
<div className="mt-4 flex justify-between items-center">
<div className="text-sm text-gray-600">
Показано {(currentPage - 1) * itemsPerPage + 1}-
{Math.min(currentPage * itemsPerPage, sortedData.length)} из{" "}
{sortedData.length}
</div>
<div className="flex gap-2">
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-3 py-1 bg-gray-300 disabled:opacity-50 rounded"
>
← Назад
</button>
<span className="px-3 py-1">Страница {currentPage} из {totalPages}</span>
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="px-3 py-1 bg-gray-300 disabled:opacity-50 rounded"
>
Вперёд →
</button>
</div>
</div>
</div>
);
}
Решение 3: Продвинутое с useCallback и кастомным хуком
// Кастомный хук для сортировки
function useSortableData<T extends Record<string, any>>(
data: T[],
defaultKey?: keyof T
) {
const [sortKey, setSortKey] = useState<keyof T | null>(defaultKey || null);
const [sortOrder, setSortOrder] = useState<SortOrder>(null);
const handleSort = useCallback((key: keyof T) => {
setSortKey((prevKey) => {
if (prevKey === key) {
setSortOrder((prevOrder) => {
if (prevOrder === "asc") return "desc";
if (prevOrder === "desc") {
setSortKey(null);
return null;
}
return "asc";
});
return prevKey;
} else {
setSortKey(key);
setSortOrder("asc");
return key;
}
});
}, []);
const sortedData = useMemo(() => {
if (!sortKey || !sortOrder) return data;
return [...data].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;
});
}, [data, sortKey, sortOrder]);
return { sortedData, sortKey, sortOrder, handleSort };
}
// Использование
function TableWithHook({ data, columns }: SortableTableProps<any>) {
const { sortedData, sortKey, sortOrder, handleSort } =
useSortableData(data);
return (
<table>
<thead>
<tr>
{columns.map((col) => (
<th key={String(col.key)} onClick={() => handleSort(col.key)}>
{col.label}
{sortKey === col.key && <span>{sortOrder === "asc" ? "↑" : "↓"}</span>}
</th>
))}
</tr>
</thead>
<tbody>
{sortedData.map((row, idx) => (
<tr key={idx}>
{columns.map((col) => (
<td key={String(col.key)}>{row[col.key]}</td>
))}
</tr>
))}
</tbody>
</table>
);
}
Пример использования
const data = [
{ id: 1, name: "Alice", age: 25, salary: 50000 },
{ id: 2, name: "Bob", age: 30, salary: 60000 },
{ id: 3, name: "Charlie", age: 20, salary: 40000 },
{ id: 4, name: "Diana", age: 28, salary: 55000 },
];
const columns = [
{ key: "name", label: "Name" },
{ key: "age", label: "Age" },
{ key: "salary", label: "Salary" },
];
// Базовая таблица
<SortableTable data={data} columns={columns} />
// С пагинацией
<SortableTableWithPagination data={data} columns={columns} itemsPerPage={2} />
Лучшие практики
- useMemo — оптимизируем пересчёты
- useCallback — стабильные функции для дочерних компонентов
- Generics — типизация для любых данных
- Функциональные компоненты — современный React
- Разделение ответственности — хуки для логики, компонент для UI
Рекомендации для собеседования
- Начните с базовой версии — показывает понимание useState
- Объясните useMemo — оптимизация важна
- Добавьте пагинацию — как бонус
- Обсудите edge cases — пустые данные, большие наборы
- Покажите кастомный хук — это выглядит профессионально
Лучший выбор для production: Вариант с кастомным хуком + пагинация — переиспользуемо и модульно.