Skip to content

Компонент

Как создавать React-компоненты внутри SLM-модулей.

Назначение

Компонент — минимальная UI-единица проекта. Это один .tsx файл без собственной папки, сегментов и публичного API.

Компонент может использовать стили, типы, хуки и другие ресурсы родительского модуля. Наличие связанных файлов в styles/ или types/ не превращает компонент в модуль.

Компонент или модуль

Классификация определяется границей владения:

  • component — один .tsx файл внутри модуля;
  • module — папка с index.ts, сегментами и собственной публичной границей.
text
user/
├── ui/
│   └── user-avatar.tsx            # компонент
├── styles/
│   └── user-avatar.module.css     # ресурс родительского модуля
├── types/
│   └── user-avatar.type.ts        # ресурс родительского модуля
└── user.tsx                       # корневой компонент модуля

user-avatar.tsx остаётся компонентом, потому что у него нет собственной папки, index.ts и сегментов.

Где размещать

Компонент размещается внутри модуля:

  • В корне модуля, если это главная UI-сущность модуля.
  • В ui/, если это вспомогательный компонент модуля.
text
user/
├── ui/
│   └── user-avatar.tsx
├── styles/
│   ├── user.module.css
│   └── user-avatar.module.css
├── types/
│   ├── user.type.ts
│   └── user-avatar.type.ts
├── user.tsx
└── index.ts

user.tsx — корневой компонент модуля. ui/user-avatar.tsx — вспомогательный компонент этого же модуля.

Что запрещено

  • Заворачивать компонент в папку: ui/header/header.tsx.
  • Создавать для компонента отдельный index.ts.
  • Создавать собственные сегменты внутри папки компонента: ui/header/styles/, ui/header/types/, ui/header/hooks/ и т.п.
  • Декларировать внутри .tsx сторы, сервисы, API-клиенты, мапперы или утилиты. Для этого есть сегменты родительского модуля.
  • Размещать бизнес-правила прямо в компоненте. Компонент может использовать готовые зависимости модуля, но не определяет их.
  • Размещать компонент в parts/ напрямую. parts/ содержит только модули.

Плохо

text
user/
└── ui/
    └── user-avatar/
        ├── styles/
        │   └── user-avatar.module.css
        ├── user-avatar.tsx
        └── index.ts

Хорошо

text
user/
├── ui/
│   └── user-avatar.tsx
├── styles/
│   └── user-avatar.module.css
└── types/
    └── user-avatar.type.ts

Стили и типы

Компонент использует ресурсы родительского модуля.

styles/ и types/ рядом с корневым компонентом — это сегменты модуля, а не собственные папки .tsx файла.

  • CSS Module компонента лежит в styles/ родительского модуля и называется по компоненту: user-avatar.module.css.
  • Если у компонента есть CSS Module, его корневой класс всегда называется .root.
  • Типы компонента лежат в types/ родительского модуля и называются по компоненту: user-avatar.type.ts.
  • Локальный type Props внутри .tsx не используется. Типы пропсов всегда выносятся в types/ родительского модуля.
  • Экспорт типа из types/ не делает его публичным API. Публичным он становится только при реэкспорте из index.ts модуля.

Причина .root: в DevTools проще находить корневой DOM-узел компонента и отличать его от внутренних элементов.

Реализация

  • Компоненты объявляются через const.
  • React.FC не используется.
  • JSDoc-комментарий обязателен для компонента.
  • Пропсы деструктурируются в теле компонента.
  • className объединяется с styles.root через cl().
  • Побочные эффекты и состояние выносятся в хуки модуля, если перестают быть тривиальными.
  • Компонент возвращает JSX и не содержит orchestration-код страницы или бизнес-домена.

user/types/user-avatar.type.ts

ts
import type { ImageProps } from 'next/image'

/**
 * Параметры UserAvatar.
 */
export type UserAvatarParams = {}

/** Пропсы базового изображения. */
type RootAttrs = ImageProps

export type UserAvatarProps = RootAttrs & UserAvatarParams

user/ui/user-avatar.tsx

tsx
import cl from 'clsx'
import Image from 'next/image'
import type { UserAvatarProps } from '../types/user-avatar.type'
import styles from '../styles/user-avatar.module.css'

/**
 * Аватар пользователя.
 *
 * Используется для:
 *  - отображения пользователя в карточке
 *  - отображения пользователя в шапке профиля
 */
export const UserAvatar = (props: UserAvatarProps) => {
  const { className, ...imageProps } = props

  return <Image {...imageProps} className={cl(styles.root, className)} />
}

user/styles/user-avatar.module.css

css
.root {
  display: block;
  border-radius: 50%;
}

Когда нужен модуль

Решение о выделении модуля остаётся за разработчиком. Поднимать компонент в модуль стоит, если он становится самостоятельной областью:

  • получает свои вложенные компоненты;
  • получает свои хуки, стор или сервисы;
  • получает внутренние мапперы или утилиты;
  • требует собственного публичного API;
  • начинает переиспользоваться вне родительского модуля;
  • становится отдельной зоной параллельной разработки.

Пример: страница — это screen-модуль, а самостоятельные секции страницы — вложенные модули в parts/.

text
screens/home/
├── parts/
│   ├── hero-section/
│   │   ├── styles/
│   │   │   └── hero-section.module.css
│   │   ├── types/
│   │   │   └── hero-section.type.ts
│   │   ├── hero-section.tsx
│   │   └── index.ts
│   └── features-section/
│       ├── styles/
│       │   └── features-section.module.css
│       ├── types/
│       │   └── features-section.type.ts
│       ├── features-section.tsx
│       └── index.ts
├── styles/
│   └── home.module.css
├── types/
│   └── home.type.ts
├── home.screen.tsx
└── index.ts

hero-section и features-section — модули, потому что это самостоятельные части страницы со своей структурой и публичной точкой входа.