← Назад к вопросам
Как организуешь структуру компонентов страницы с header, таблицей и footer?
2.3 Middle🔥 191 комментариев
#React
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как организовать структуру компонентов: header, таблица, footer
Это типичная структура приложения. Ключ - правильное разделение ответственности и переиспользование компонентов.
Принцип организации
Я следую этому порядку:
- Layouts - каркас страницы (header, footer, sidebar)
- Pages - конкретные страницы
- Features - фичи (таблицы, формы, галереи)
- 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>© 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>
);
}
Ключевые принципы
-
Layout - только структура
- Header, Footer, Sidebar
- Не содержит бизнес-логику
- Переиспользуется для всех страниц
-
Features - логика фичи
- Таблица, форма, фильтры
- Содержит состояние (useState, useContext)
- Кастомные хуки для API запросов
-
UI Components - переиспользуемые
- Button, Input, Card
- Только props, без логики
- Максимально независимые
-
Separation of Concerns
- Компонент отвечает за ONE thing
- Логику в кастомные хуки
- Стили в Tailwind классы
-
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 - переиспользуемый компонент строки
Этот подход масштабируется и легко тестируется.