Container и Presentational
Container/Presentational (Smart/Dumb) — паттерн разделения компонентов UI на «умные» (Container), управляющие данными и логикой, и «глупые» (Presentational), занимающиеся только отображением.
Зачем нужно
Смешение логики и отображения делает компоненты нетестируемыми и трудночитаемыми. Presentational-компоненты — чистые функции: получили props, вернули JSX. Их можно тестировать без Redux, Router или API. Container-компоненты инкапсулируют side-эффекты и передают данные вниз. В современном React с хуками граница размылась, но разделение ответственности остаётся важным принципом.
Где используется
- React-приложения: разделение компонентов на «умные» и «глупые»
- Vue, Angular: аналогичное разделение smart/dumb компонентов
- Storybook: Presentational-компоненты документируются и тестируются независимо
- Design System: UI-компоненты без бизнес-логики
- Любые UI-фреймворки с компонентной архитектурой
Основной контент
Классическое разделение
// Presentational — только отображение, получает данные через props
// Не знает о Redux, Router, API
function UserCard({ name, email, avatar, onEdit }) {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>{email}</p>
<button onClick={onEdit}>Редактировать</button>
</div>
);
}
// Container — данные, логика, side-эффекты
function UserCardContainer({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const navigate = useNavigate;
useEffect(() => {
fetchUser(userId)
.then(setUser)
.finally( => setLoading(false));
}, [userId]);
if (loading) return <Spinner />;
if (!user) return <NotFound />;
return (
<UserCard
name={user.name}
email={user.email}
avatar={user.avatarUrl}
onEdit={ => navigate(`/users/${userId}/edit`)}
/>
);
}
Современный подход с Custom Hooks
// В современном React логику выносят в хук, а не в Container-компонент
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(setUser)
.catch(setError)
.finally( => setLoading(false));
}, [userId]);
return { user, loading, error };
}
// Компонент использует хук — остаётся относительно «глупым»
function UserPage({ userId }) {
const { user, loading, error } = useUser(userId);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <UserCard user={user} />;
}
Тестирование Presentational-компонентов
// UserCard легко тестировать — нет зависимостей от Store/Router/API
import { render, screen, fireEvent } from '@testing-library/react';
test('UserCard показывает имя и email', () => {
const onEdit = jest.fn;
render(
<UserCard
name="Иван Иванов"
email="ivan@example.com"
avatar="/avatar.jpg"
onEdit={onEdit}
/>
);
expect(screen.getByText('Иван Иванов')).toBeInTheDocument;
expect(screen.getByText('ivan@example.com')).toBeInTheDocument;
fireEvent.click(screen.getByText('Редактировать'));
expect(onEdit).toHaveBeenCalled();
});
Частые ошибки
- Слишком много Container-компонентов: каждый компонент с доступом к Store снижает переиспользуемость. Данные лучше опускать через props или контекст.
- Бизнес-логика в Presentational: обработчик события не должен напрямую вызывать API — только колбэк из props.
- Жёсткое следование паттерну с хуками: Дэн Абрамов паттерна, признал, что с хуками граница размылась — не создавайте Container-компонент ради паттерна, если хук решает задачу элегантнее.