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

Создать React-компонент таблицы с сортировкой

2.0 Middle🔥 181 комментариев
#React

Условие

Создайте React-компонент таблицы с возможностью сортировки по столбцам.

Требования

  1. Компонент принимает массив данных и конфигурацию столбцов
  2. При клике на заголовок столбца - сортировка по возрастанию
  3. При повторном клике - сортировка по убыванию
  4. Показывать индикатор направления сортировки
  5. Использовать функциональные компоненты и хуки

Пример использования

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} />

Лучшие практики

  1. useMemo — оптимизируем пересчёты
  2. useCallback — стабильные функции для дочерних компонентов
  3. Generics — типизация для любых данных
  4. Функциональные компоненты — современный React
  5. Разделение ответственности — хуки для логики, компонент для UI

Рекомендации для собеседования

  1. Начните с базовой версии — показывает понимание useState
  2. Объясните useMemo — оптимизация важна
  3. Добавьте пагинацию — как бонус
  4. Обсудите edge cases — пустые данные, большие наборы
  5. Покажите кастомный хук — это выглядит профессионально

Лучший выбор для production: Вариант с кастомным хуком + пагинация — переиспользуемо и модульно.