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

Как организуешь структуру компонентов страницы с header, таблицей и footer?

2.3 Middle🔥 191 комментариев
#React

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как организовать структуру компонентов: header, таблица, footer

Это типичная структура приложения. Ключ - правильное разделение ответственности и переиспользование компонентов.

Принцип организации

Я следую этому порядку:

  1. Layouts - каркас страницы (header, footer, sidebar)
  2. Pages - конкретные страницы
  3. Features - фичи (таблицы, формы, галереи)
  4. UI Components - переиспользуемые кубики

Структура файлов

src/
  components/
    layout/
      Header.tsx
      Footer.tsx
      PageLayout.tsx
    features/
      users/
        UserTable.tsx
        UserTableRow.tsx
        UserTableHeader.tsx
        useUserTable.ts (кастомный хук)
    ui/
      Button.tsx
      Table.tsx
      TableCell.tsx
  app/
    users/
      page.tsx (Next.js Page Router или файл компонента)

Компонент Layout (каркас)

// components/layout/PageLayout.tsx
import React from 'react';
import { Header } from './Header';
import { Footer } from './Footer';

interface PageLayoutProps {
  children: React.ReactNode;
  title?: string;
}

export function PageLayout({ children, title }: PageLayoutProps) {
  return (
    <div className="flex flex-col min-h-screen">
      <Header />
      
      <main className="flex-1 px-4 py-8 max-w-6xl mx-auto w-full">
        {title && <h1 className="text-3xl font-bold mb-6">{title}</h1>}
        {children}
      </main>
      
      <Footer />
    </div>
  );
}

Header компонент

// components/layout/Header.tsx
import Link from 'next/link';
import { useAuth } from '@/hooks/useAuth';

export function Header() {
  const { user, logout } = useAuth();

  return (
    <header className="bg-surface-primary border-b border-border-primary sticky top-0 z-50">
      <div className="max-w-6xl mx-auto px-4 py-4 flex justify-between items-center">
        <Link href="/" className="text-2xl font-bold text-content-primary">
          PrepBro
        </Link>
        
        <nav className="flex gap-6 items-center">
          <Link href="/questions" className="text-content-secondary hover:text-content-primary">
            Questions
          </Link>
          <Link href="/professions" className="text-content-secondary hover:text-content-primary">
            Professions
          </Link>
          
          {user ? (
            <div className="flex gap-4 items-center">
              <span className="text-content-secondary">Hello, {user.name}</span>
              <button
                onClick={logout}
                className="px-4 py-2 bg-bg-surface-secondary rounded-lg hover:bg-surface-hover"
              >
                Logout
              </button>
            </div>
          ) : (
            <Link href="/auth" className="text-link hover:underline">
              Sign In
            </Link>
          )}
        </nav>
      </div>
    </header>
  );
}

Footer компонент

// components/layout/Footer.tsx
export function Footer() {
  return (
    <footer className="bg-surface-secondary border-t border-border-primary py-8 mt-16">
      <div className="max-w-6xl mx-auto px-4">
        <div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
          <div>
            <h3 className="font-bold text-content-primary mb-4">Product</h3>
            <ul className="space-y-2">
              <li><a href="#" className="text-link hover:underline">Features</a></li>
              <li><a href="#" className="text-link hover:underline">Pricing</a></li>
              <li><a href="#" className="text-link hover:underline">Blog</a></li>
            </ul>
          </div>
          
          <div>
            <h3 className="font-bold text-content-primary mb-4">Company</h3>
            <ul className="space-y-2">
              <li><a href="#" className="text-link hover:underline">About</a></li>
              <li><a href="#" className="text-link hover:underline">Careers</a></li>
              <li><a href="#" className="text-link hover:underline">Contact</a></li>
            </ul>
          </div>
          
          <div>
            <h3 className="font-bold text-content-primary mb-4">Legal</h3>
            <ul className="space-y-2">
              <li><a href="#" className="text-link hover:underline">Privacy</a></li>
              <li><a href="#" className="text-link hover:underline">Terms</a></li>
              <li><a href="#" className="text-link hover:underline">Cookie Policy</a></li>
            </ul>
          </div>
        </div>
        
        <div className="border-t border-border-primary pt-8 text-center text-content-secondary">
          <p>&copy; 2026 PrepBro. All rights reserved.</p>
        </div>
      </div>
    </footer>
  );
}

Таблица - отдельный компонент

// components/features/users/UserTable.tsx
import { useEffect, useState } from 'react';
import { useUserTable } from './useUserTable';
import { UserTableHeader } from './UserTableHeader';
import { UserTableRow } from './UserTableRow';

interface User {
  id: string;
  name: string;
  email: string;
  profession: string;
  createdAt: string;
}

export function UserTable() {
  const {
    users,
    loading,
    error,
    sortBy,
    setSortBy,
    filterBy,
    setFilterBy,
    pagination,
    fetchUsers
  } = useUserTable();

  useEffect(() => {
    fetchUsers();
  }, [filterBy, sortBy, pagination.page]);

  if (loading) return <div className="text-center py-8">Loading...</div>;
  if (error) return <div className="text-red-500 py-8">{error}</div>;

  return (
    <div className="space-y-4">
      {/* Фильтры и поиск */}
      <div className="flex gap-4 mb-4">
        <input
          type="text"
          placeholder="Search by name..."
          value={filterBy}
          onChange={(e) => setFilterBy(e.target.value)}
          className="flex-1 px-4 py-2 border border-border-primary rounded-lg"
        />
      </div>

      {/* Таблица */}
      <div className="overflow-x-auto border border-border-primary rounded-lg">
        <table className="w-full">
          <UserTableHeader sortBy={sortBy} setSortBy={setSortBy} />
          <tbody>
            {users.map(user => (
              <UserTableRow key={user.id} user={user} />
            ))}
          </tbody>
        </table>
      </div>

      {/* Пагинация */}
      <div className="flex justify-between items-center">
        <button
          disabled={pagination.page === 1}
          onClick={() => pagination.setPage(pagination.page - 1)}
          className="px-4 py-2 bg-bg-surface-secondary disabled:opacity-50"
        >
          Previous
        </button>
        
        <span className="text-content-secondary">
          Page {pagination.page} of {pagination.totalPages}
        </span>
        
        <button
          disabled={pagination.page === pagination.totalPages}
          onClick={() => pagination.setPage(pagination.page + 1)}
          className="px-4 py-2 bg-bg-surface-secondary disabled:opacity-50"
        >
          Next
        </button>
      </div>
    </div>
  );
}

Кастомный хук для таблицы

// components/features/users/useUserTable.ts
import { useState, useCallback } from 'react';
import { api } from '@/lib/api';

interface User {
  id: string;
  name: string;
  email: string;
  profession: string;
  createdAt: string;
}

export function useUserTable() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [sortBy, setSortBy] = useState<'name' | 'date'>('date');
  const [filterBy, setFilterBy] = useState('');
  const [page, setPage] = useState(1);
  const [totalPages, setTotalPages] = useState(1);

  const fetchUsers = useCallback(async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await api.get('/api/v1/users', {
        params: {
          page,
          search: filterBy,
          sort: sortBy
        }
      });
      
      setUsers(response.data.items);
      setTotalPages(response.data.totalPages);
    } catch (err) {
      setError('Failed to load users');
      console.error(err);
    } finally {
      setLoading(false);
    }
  }, [page, filterBy, sortBy]);

  return {
    users,
    loading,
    error,
    sortBy,
    setSortBy,
    filterBy,
    setFilterBy,
    pagination: { page, setPage, totalPages },
    fetchUsers
  };
}

Строка таблицы

// components/features/users/UserTableRow.tsx
import { User } from '@/types/user';
import { formatDate } from '@/lib/utils';

interface UserTableRowProps {
  user: User;
}

export function UserTableRow({ user }: UserTableRowProps) {
  return (
    <tr className="border-b border-border-primary hover:bg-surface-hover">
      <td className="px-4 py-3 text-content-primary">{user.name}</td>
      <td className="px-4 py-3 text-content-secondary">{user.email}</td>
      <td className="px-4 py-3 text-content-secondary">{user.profession}</td>
      <td className="px-4 py-3 text-content-secondary text-sm">
        {formatDate(user.createdAt)}
      </td>
      <td className="px-4 py-3 text-right">
        <button className="text-link hover:underline">View</button>
      </td>
    </tr>
  );
}

Использование на странице

// app/users/page.tsx
import { PageLayout } from '@/components/layout/PageLayout';
import { UserTable } from '@/components/features/users/UserTable';

export default function UsersPage() {
  return (
    <PageLayout title="Users">
      <UserTable />
    </PageLayout>
  );
}

Ключевые принципы

  1. Layout - только структура

    • Header, Footer, Sidebar
    • Не содержит бизнес-логику
    • Переиспользуется для всех страниц
  2. Features - логика фичи

    • Таблица, форма, фильтры
    • Содержит состояние (useState, useContext)
    • Кастомные хуки для API запросов
  3. UI Components - переиспользуемые

    • Button, Input, Card
    • Только props, без логики
    • Максимально независимые
  4. Separation of Concerns

    • Компонент отвечает за ONE thing
    • Логику в кастомные хуки
    • Стили в Tailwind классы
  5. Props Drilling vs Context

    • Если 1-2 уровня - props
    • Если 3+ уровня - useContext

Тестирование структуры

// __tests__/UserTable.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserTable } from '@/components/features/users/UserTable';

jest.mock('@/lib/api');

describe('UserTable', () => {
  it('should render users after loading', async () => {
    render(<UserTable />);
    
    expect(screen.getByText('Loading...')).toBeInTheDocument();
    
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
    });
  });

  it('should filter users when searching', async () => {
    render(<UserTable />);
    const input = screen.getByPlaceholderText('Search by name...');
    
    await userEvent.type(input, 'Jane');
    
    await waitFor(() => {
      expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
    });
  });
});

Вывод

Структура страницы с header, таблицей, footer:

  • PageLayout - каркас (header + main + footer)
  • UserTable - фича с логикой
  • useUserTable - хук для API и состояния
  • UserTableRow - переиспользуемый компонент строки

Этот подход масштабируется и легко тестируется.

Как организуешь структуру компонентов страницы с header, таблицей и footer? | PrepBro