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

Как оформлял контент в виде таблицы?

1.3 Junior🔥 101 комментариев
#HTML и CSS

Комментарии (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>
  );
}

Рекомендация

Для таблиц используй:

  1. Типизированные данные (TypeScript)
  2. Кастомные хуки (usePagination, useTableState, useTableSelection)
  3. Композиция компонентов (отделённые TableHeader, TableRow, TableCell)
  4. Доступность (role, aria-label, scope)
  5. Производительность (useMemo для больших наборов данных)