Назад
Назад к заметкам
· 4 мин чтения

Проектировать переиспользуемые UI-компоненты: практический набор паттернов

Пошаговые практики создания переиспользуемых UI-компонентов, которые ускоряют разработку, упрощают тестирование и позволяют менять стили без лишних правок.

reactcomponentsui-designreusabilitypatterns

Если вы уже сталкивались с тем, что одна кнопка в разных местах проекта выглядит одинаково, но каждый раз нужно менять стили, тесты и типы вы знаете, как быстро растут технические долги. Именно поэтому стоит научиться проектировать переиспользуемые UI-компоненты: экономия времени, единообразие и простота тестирования становятся ощутимыми сразу после первого удачного рефакторинга.

В статье разберем набор практик, которые делают UI-компоненты переиспользуемыми без лишних абстракций. Все, что нужно знать, собрал в виде коротких секций, каждый из которых можно внедрять независимо.


1. Выделить единую ответственность (SRP) фундамент переиспользования

Самый простой способ “нарушить” переиспользуемость добавить в компонент бизнес-логику. По правилу принцип единой ответственности компонент должен делать только то, что относится к UI: принимать данные, отрисовывать их и генерировать события.

// ❌ Плохой пример: компонент отвечает и за отображение, и за запрос данных
function UserCard({ userId }) {
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((r) => r.json())
      .then(setUser);
  }, [userId]);

  if (!user) return <Spinner />;
  return (
    <div className="card">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}
// ✅ Хороший пример: UI-компонент получает готовый объект и только рендерит
function UserCard({ user }) {
  return (
    <div className="card">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

Почему это важно: такой подход делает компонент чистым, его легко тестировать и переиспользовать в разных местах, где данные могут приходить из разных источников (REST, GraphQL, локальный стейт).

Подробнее о принципе DRY


2. Компонент-контейнер vs. Компонент-презентер

Разделение ответственности выглядит еще более очевидно, если вынести бизнес-логику в отдельный контейнерный компонент. Презентер остается “декорацией”, а контейнер управляет состоянием и взаимодействует с API. Это типичная ситуация, где переиспользуемые UI-компоненты получают универсальную “обертку”.

ПрезентерКонтейнер
ЦельОтображение UIУправление данными
ПропсыСтатические данные, коллбэкиЗапросы, обработка событий
ТестыСнэпшот-тесты, визуальныеЮнит-тесты бизнес-логики
// Презентер
function Button({ children, onClick, disabled }) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {children}
    </button>
  );
}

// Контейнер
function SubmitButton({ onSubmit }) {
  const [loading, setLoading] = useState(false);
  const handleClick = async () => {
    setLoading(true);
    await onSubmit();
    setLoading(false);
  };
  return (
    <Button onClick={handleClick} disabled={loading}>
      {loading ? "Отправка…" : "Отправить"}
    </Button>
  );
}

Эта схема упрощает переиспользование: любой UI-компонент (Button) может быть “зажат” в разных контейнерах, меняя лишь бизнес-логику.


3. Параметризация через стили и темы

Часто переиспользуемость блокируется жестко прописанными CSS-классами. Решение делать стили параметричными и поддерживать тематизацию. В React популярны библиотеки типа styled-components и emotion, но даже без них можно использовать CSS-модули.

// styled-components пример
import styled from "styled-components";

const Card = styled.div`
  background: ${({ theme }) => theme.bg};
  border-radius: 8px;
  padding: ${({ padding }) => padding || "16px"};
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
`;

function InfoCard({ title, children, theme, padding }) {
  return (
    <Card theme={theme} padding={padding}>
      <h3>{title}</h3>
      {children}
    </Card>
  );
}

Тема может приходить из контекста (ThemeProvider), а параметры из пропсов. Таким образом один компонент покрывает разные визуальные варианты без дублирования кода, оставаясь переиспользуемым.


4. Универсальная типизация и “props-spreading”

Для переиспользования важно, чтобы компонент мог получать любые атрибуты HTML. Вместо перечисления всех возможных пропсов используйте ...rest и типизацию React.HTMLAttributes<HTMLDivElement>.

type BoxProps = React.HTMLAttributes<HTMLDivElement> & {
  as?: keyof JSX.IntrinsicElements; // позволяет менять тег
  border?: string;
};

const Box: React.FC<BoxProps> = ({
  as: Component = "div",
  border,
  style,
  ...rest
}) => <Component style={{ border, ...style }} {...rest} />;

Теперь Box можно использовать как <Box as="section" border="1px solid #eee" onClick={...}> и он автоматически получит все валидные HTML-атрибуты. Это избавляет от необходимости писать “обертку” под каждый новый случай.


5. Тестируемость как показатель переиспользуемости

Если компонент слишком “связан”, писать юнит-тесты будет больно. Пример простого теста с @testing-library/react позволяет быстро проверить работу без моков.

import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import SubmitButton from "./SubmitButton";

test("отображает загрузку при клике", async () => {
  const mockSubmit = jest.fn().mockResolvedValueOnce(undefined);
  render(<SubmitButton onSubmit={mockSubmit} />);
  fireEvent.click(screen.getByText("Отправить"));
  expect(screen.getByText("Отправка…")).toBeInTheDocument();
  await waitFor(() => expect(mockSubmit).toHaveBeenCalled());
});

Тест проверяет лишь поведение, а не детали реализации. Если компонент соблюдает SRP и использует пропсы, такой тест пишется за пару минут, а не часы.


FAQ / Чеклист

  • Не смешивайте логику и разметку. Перенесите запросы в контейнер или хук.
  • Параметризуйте стили, а не фиксируйте цвета в коде. Тема ваш лучший друг.
  • Избегайте “жесткого” кастомного API. Делайте компонент максимально “прозрачным” для HTML-атрибутов.
  • Проверяйте типы. Правильная типизация уберет множество ошибок на этапе разработки.
  • Пишите простой snapshot-тест или тест на поведение. Если тестов нет шанс, что компонент не переиспользуемый.

Переиспользуемый UI-компонент это набор привычек: чистый интерфейс, гибкая стилизация и строгая типизация. Когда код устроен так, добавить новую кнопку, форму или карточку в любой части проекта становится вопросом нескольких строк, а не полного рефакторинга.

Если работаете с формами, также полезно изучить контролируемые и неконтролируемые компоненты.

Готов начать?

Выбери удобный способ связи — напиши напрямую или оставь заявку

Написать в Telegram

Отвечу в течение пары часов