← Назад к вопросам

Как отделить UIkit от основного продукта?

1.0 Junior🔥 61 комментариев
#Soft Skills и рабочие процессы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как отделить UIkit от основного продукта

Отделение UIkit (компонентной библиотеки) от основного приложения — это важная архитектурная задача для масштабируемости. Давайте разберём стратегии.

Что такое UIkit

UIkit — это набор переиспользуемых компонентов (Button, Input, Card, Modal и т.д.), которые используются на всех страницах приложения. Отделить их означает:

  • Независимое развитие и версионирование
  • Переиспользование в других проектах
  • Снижение связанности (coupling)
  • Четкий контракт между UIkit и приложением

Способ 1: Отдельная папка (для одного проекта)

Подходит, если UIkit используется только в одном проекте, но нужна четкая граница.

Структура:

project/
├── src/
│   ├── uikit/           <-- Отдельная папка для компонентов
│   │   ├── Button/
│   │   ├── Input/
│   │   ├── Card/
│   │   └── index.ts     <-- Экспорт всех компонентов
│   ├── features/        <-- Специфичные для приложения
│   │   ├── auth/
│   │   ├── profile/
│   │   └── dashboard/
│   └── App.tsx

Файл uikit/index.ts:

// Единая точка входа для всех UI компонентов
export { Button } from './Button';
export { Input } from './Input';
export { Card } from './Card';
export { Modal } from './Modal';
export { Checkbox } from './Checkbox';

// Типы
export type { ButtonProps } from './Button';
export type { InputProps } from './Input';

Использование в приложении:

// Импортируем только из uikit, не из внутренних папок
import { Button, Input, Card } from '@/uikit';

function LoginPage() {
  return (
    <Card>
      <Input placeholder="Email" />
      <Input type="password" placeholder="Password" />
      <Button variant="primary">Login</Button>
    </Card>
  );
}

Преимущества:

  • Простая в реализации
  • Единое место для всех компонентов
  • Легко найти и обновлять

Недостатки:

  • Сложнее переиспользовать в других проектах
  • UIkit растет вместе с основным проектом

Способ 2: Monorepo с разными пакетами

Подходит, если нужна полная независимость UIkit для переиспользования в разных проектах.

Структура (pnpm workspaces или yarn):

monorepo/
├── packages/
│   ├── ui-kit/          <-- NPM пакет с компонентами
│   │   ├── src/
│   │   │   ├── Button/
│   │   │   ├── Input/
│   │   │   └── index.ts
│   │   ├── package.json
│   │   ├── tsconfig.json
│   │   └── .storybook/
│   ├── app-main/        <-- Основное приложение
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── app-admin/       <-- Другое приложение
│       ├── src/
│       └── package.json
└── pnpm-workspace.yaml

packages/ui-kit/package.json:

{
  "name": "@mycompany/ui-kit",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    },
    "./styles": "./dist/styles.css"
  },
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

Использование в приложении:

// Импортируем из отдельного пакета
import { Button, Input, Card } from '@mycompany/ui-kit';
import '@mycompany/ui-kit/styles'; // Стили компонентов

function LoginPage() {
  return (
    <Card>
      <Input placeholder="Email" />
      <Button>Login</Button>
    </Card>
  );
}

Преимущества:

  • UIkit полностью независим
  • Можно версионировать отдельно
  • Переиспользуем в разных проектах
  • Четкий контракт через package.json
  • Можно развивать параллельно

Недостатки:

  • Сложнее в настройке
  • Нужна система управления версиями
  • CI/CD должна уметь собирать пакеты

Способ 3: Отдельный npm пакет (публичный или приватный)

Самый чистый способ, если нужна максимальная независимость.

Отдельный репозиторий:

ui-kit-repo/
├── src/
│   ├── Button/
│   │   ├── Button.tsx
│   │   ├── Button.styles.ts
│   │   ├── Button.test.tsx
│   │   └── index.ts
│   └── index.ts
├── package.json
├── tsconfig.json
├── .storybook/
│   └── main.ts          <-- Документация компонентов
└── README.md

Подключение через package.json:

{
  "dependencies": {
    "@mycompany/ui-kit": "^1.2.0"
  }
}

Или для приватного пакета:

{
  "dependencies": {
    "@mycompany/ui-kit": "github:mycompany/ui-kit#main"
  }
}

Архитектурные правила для UIkit

1. UIkit не должен знать о features приложения

// ПЛОХО: Button зависит от auth features
import { useAuthStore } from '@/features/auth';

export function Button(props) {
  const { isLoggedIn } = useAuthStore();
  // UIkit не должен знать о auth
}

// ХОРОШО: Button принимает всё через props
export interface ButtonProps {
  onClick?: () => void;
  disabled?: boolean;
  children: React.ReactNode;
  variant?: 'primary' | 'secondary';
}

export function Button({ onClick, children, ...props }: ButtonProps) {
  return <button onClick={onClick} {...props}>{children}</button>;
}

2. UIkit определяет только UI, не бизнес-логику

// ПЛОХО: UIkit контролирует валидацию (бизнес-логика)
function Input(props) {
  const [error, setError] = useState('');
  
  const handleChange = (e) => {
    if (!isValidEmail(e.target.value)) {
      setError('Invalid email'); // Это бизнес-логика!
    }
  };
}

// ХОРОШО: Компонент просто отображает ошибку
function Input({ error, ...props }: InputProps) {
  return (
    <div>
      <input {...props} />
      {error && <span className="error">{error}</span>}
    </div>
  );
}

// Валидация в features/auth
function LoginForm() {
  const [email, setEmail] = useState('');
  const [emailError, setEmailError] = useState('');
  
  const handleEmailChange = (value) => {
    setEmail(value);
    setEmailError(isValidEmail(value) ? '' : 'Invalid email');
  };
  
  return <Input value={email} error={emailError} onChange={handleEmailChange} />;
}

3. Явные зависимости через props (Dependency Injection)

// ПЛОХО: компонент импортирует из store
import { useUserStore } from '@/store';

function UserCard() {
  const user = useUserStore();
  return <div>{user.name}</div>;
}

// ХОРОШО: user передается через props
function UserCard({ user }: { user: User }) {
  return <div>{user.name}</div>;
}

// Использование в приложении
import { useUserStore } from '@/store';

function ProfilePage() {
  const user = useUserStore();
  return <UserCard user={user} />; // Явная зависимость
}

Пример структуры UIkit компонента

// uikit/Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button';

// uikit/Button/Button.tsx
import { cn } from '@/lib/utils';
import './Button.css';

export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  loading?: boolean;
  children: React.ReactNode;
}

export const Button = ({
  variant = 'primary',
  size = 'md',
  loading,
  children,
  className,
  ...props
}: ButtonProps) => {
  return (
    <button
      className={cn(
        'button',
        `button--${variant}`,
        `button--${size}`,
        { 'button--loading': loading },
        className
      )}
      disabled={loading || props.disabled}
      {...props}
    >
      {loading ? 'Loading...' : children}
    </button>
  );
};

// uikit/Button/Button.test.tsx
import { render, screen } from '@testing-library/react';
import { Button } from './Button';

test('renders with different variants', () => {
  render(<Button variant="primary">Click me</Button>);
  expect(screen.getByRole('button')).toHaveClass('button--primary');
});

test('shows loading state', () => {
  render(<Button loading>Submit</Button>);
  expect(screen.getByRole('button')).toBeDisabled();
  expect(screen.getByText('Loading...')).toBeInTheDocument();
});

Управление версиями UIkit

// package.json основного приложения
{
  "dependencies": {
    "@mycompany/ui-kit": "^1.2.0"
  }
}

Semantic Versioning:

  • 1.2.3 = major.minor.patch
  • Patch (1.2.3 -> 1.2.4): баг-фиксы, совместимо
  • Minor (1.2.3 -> 1.3.0): новые фичи, совместимо
  • Major (1.2.3 -> 2.0.0): breaking changes, несовместимо

Документирование UIkit (Storybook)

// uikit/.storybook/stories/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from '../../src/Button';

const meta: Meta<typeof Button> = {
  title: 'UIKit/Button',
  component: Button,
};

export default meta;
type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'Click me',
  },
};

export const Loading: Story = {
  args: {
    loading: true,
    children: 'Submit',
  },
};

Лучшие практики

  1. Единственная точка входа (index.ts)
  2. Документирование (Storybook, JSDoc)
  3. Полное покрытие тестами (unit + visual)
  4. Чистые props (не зависит от контекста)
  5. Типизация (TypeScript strict mode)
  6. CSS инкапсуляция (CSS Modules или Tailwind)
  7. Версионирование (semantic versioning)
  8. CHANGELOG (что изменилось в новой версии)

Вывод

Выбирай способ отделения в зависимости от масштаба:

  • Маленький проект (один продукт) → Папка uikit/
  • Средний проект (несколько приложений) → Monorepo с пакетами
  • Большой проект (переиспользование во многих местах) → Отдельный npm пакет

Главное правило: UIkit зависит от nothing, всё зависит от UIkit. Это позволяет развивать компоненты независимо и переиспользовать их везде.

Как отделить UIkit от основного продукта? | PrepBro