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-компонент ради паттерна, если хук решает задачу элегантнее.

Связанные темы

Ресурсы