Проектировать переиспользуемые UI-компоненты: практический набор паттернов
Пошаговые практики создания переиспользуемых UI-компонентов, которые ускоряют разработку, упрощают тестирование и позволяют менять стили без лишних правок.
Если вы уже сталкивались с тем, что одна кнопка в разных местах проекта выглядит одинаково, но каждый раз нужно менять стили, тесты и типы вы знаете, как быстро растут технические долги. Именно поэтому стоит научиться проектировать переиспользуемые 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, локальный стейт).
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-компонент это набор привычек: чистый интерфейс, гибкая стилизация и строгая типизация. Когда код устроен так, добавить новую кнопку, форму или карточку в любой части проекта становится вопросом нескольких строк, а не полного рефакторинга.
Если работаете с формами, также полезно изучить контролируемые и неконтролируемые компоненты.