Files
frontend-style-guide/RULES.md
2026-01-30 13:21:04 +03:00

25 KiB
Raw Permalink Blame History

Технологии и библиотеки

Базовый стек технологий и библиотек, на который опираются проекты и примеры в документации.

Что используем обычно

  • TypeScript — типизация всей логики и компонентов.
  • FSD (Feature-Sliced Design) — структура проекта и границы модулей.
  • React + Next.js — основной стек для UI и приложения.
  • Mantine UI — базовые UI-компоненты.
  • SWR — получение данных по GET (кеширование и revalidate).
  • Zustand — глобальное состояние.
  • i18next (i18n) — локализация всех пользовательских текстов.
  • Vitest — тестирование.
  • clsx — конкатенация CSSклассов.
  • PostCSS Modules — изоляция стилей.
  • Mobile First — подход к адаптивной верстке.

Архитектура

Архитектура построена на FSD (FeatureSliced Design) и строгих границах модулей. Цель — разделить ответственность, упростить сопровождение и контроль зависимостей.

Принципы

  • Разделять UI, бизнес-логику и инфраструктуру.
  • Держать зависимости однонаправленными.
  • Открывать наружу только публичный API модулей.
  • Не допускать циклических зависимостей.

Слои

  • app — инициализация приложения, роутинг, конфигурации, глобальные провайдеры.
  • screens — экраны и их композиция.
  • layouts — каркас и шаблоны страниц.
  • widgets — крупные блоки интерфейса, собирающие несколько сценариев.
  • features — отдельные пользовательские действия и сценарии.
  • entities — бизнес-сущности и их модель.
  • shared — переиспользуемая инфраструктура, утилиты и базовые UIкомпоненты.

Правила зависимостей

  • Допустимые импорты идут сверху вниз: app → screens → layouts → widgets → features → entities → shared.
  • Импорты между слоями — через публичный API.
  • Внутри одного слоя — относительные импорты.

Публичный API модулей

  • Каждый модуль экспортирует наружу только то, что нужно другим слоям.
  • Внешние импорты идут только через index‑файл модуля.
  • Внутренние файлы не импортируются напрямую извне.

Границы ответственности

  • Бизнес‑логика не размещается в UIкомпонентах.
  • UIкомпоненты должны быть максимально простыми и предсказуемыми.
  • Связь между независимыми сценариями поднимается на уровень выше.

Типовые ошибки

  • Импорт из более высокого слоя в более низкий.
  • Смешивание логики нескольких слоёв в одном модуле.
  • Прямые импорты внутренних файлов, минуя публичный API.

Стиль кода

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

Отступы

  • 2 пробела (не табы).

Длина строк

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

Хорошо

const config = createRequestConfig(
  endpoint,
  {
    headers: {
      'X-Request-Id': requestId,
      'X-User-Id': userId,
    },
    params: {
      page,
      pageSize,
      sort: 'createdAt',
    },
  },
  timeoutMs,
);

Плохо

// Плохо: длинная строка с вложенными структурами плохо читается.
const config = createRequestConfig(endpoint, { headers: { 'X-Request-Id': requestId, 'X-User-Id': userId }, params: { page, pageSize, sort: 'createdAt' } }, timeoutMs);

Кавычки

  • В JavaScript/TypeScript использовать одинарные кавычки.
  • В JSX/TSX для атрибутов использовать двойные кавычки.
  • Шаблонные строки использовать только при интерполяции или многострочном тексте.

Хорошо

const label = 'Сохранить';
const title = `Привет, ${name}`;
<input type="text" placeholder="Введите имя" />

Плохо

// Плохо: двойные кавычки в TS и конкатенация вместо шаблонной строки.
const label = "Сохранить";
const title = 'Привет, ' + name;
// Плохо: одинарные кавычки в JSX-атрибутах.
<input type='text' placeholder='Введите имя' />

Точки с запятой и запятые

  • Допускаются упущения точки с запятой, если код остаётся читаемым и однозначным.
  • В многострочных массивах, объектах и параметрах функции запятая в конце допускается, но не обязательна.

Импорты

  • В именованных импортах использовать пробелы внутри фигурных скобок.
  • Типы импортировать через import type.
  • default импорт и экспорт избегать, использовать именованные.
  • Избегать импорта всего модуля через *.

Хорошо

import { MyComponent } from 'MyComponent';
import type { User } from '../model/types';

Плохо

// Плохо: default импорт и отсутствие пробелов в именованном импорте.
import MyComponent from 'MyComponent';
import type {User} from '../model/types';

Ранние возвраты (early return)

  • Использовать ранние возвраты для упрощения чтения.
  • Избегать else после return.

Хорошо

const getName = (user?: { name: string }) => {
  if (!user) {
    return 'Гость';
  }

  return user.name;
};

Плохо

// Плохо: лишний else после return усложняет чтение.
const getName = (user?: { name: string }) => {
  if (user) {
    return user.name;
  } else {
    return 'Гость';
  }
};

Форматирование объектов и массивов

  • В многострочных объектах каждое свойство на новой строке.
  • В многострочных массивах каждый элемент на новой строке.
  • Объекты и массивы можно писать в одну строку, если длина строки не превышает 100 символов.
  • В однострочных объектах и массивах использовать пробелы после запятых.

Хорошо

const roles = ['admin', 'editor', 'viewer'];
const options = { id: 1, name: 'User' };

const config = {
  url: '/api/users',
  method: 'GET',
  params: { page: 1, pageSize: 20 },
};

Плохо

// Плохо: нет пробелов после запятых и объект слишком длинный для одной строки.
const roles = ['admin','editor','viewer'];
const options = { id: 1,name: 'User' };
const config = { url: '/api/users', method: 'GET', params: { page: 1, pageSize: 20 } };

Именование

Именование должно быть предсказуемым, коротким и отражать смысл сущности.

Базовые правила

Что Рекомендуется
Папки kebab-case
Файлы kebab-case
Переменные camelCase
Константы SCREAMING_SNAKE_CASE
Классы PascalCase
React-компоненты PascalCase
Хуки useSomething
CSS классы camelCase

Архитектурный неймспейс

Соглашение о суффиксах, которые обозначают слой (уровень абстракции), роль или тип файла.

  • Суффиксы используются для обозначения слоя, роли или типа файла.
  • Суффиксы всегда пишутся в единственном числе.
  • Формат имени: name.<suffix>.ts или name.<suffix>.tsx.

UI и слои FSD

  • .screen.tsx — экран
  • .layout.tsx — layout
  • .widget.tsx — виджет
  • .feature.tsx — UI фичи
  • .entity.tsx — UI сущности
  • .ui.tsx — UIкомпонент

Логика и модель

  • .store.ts — стор
  • .service.ts — сервис

Типы и контракты

  • .type.ts — типы и интерфейсы
  • .interface.ts — файл с интерфейсами (если нужен отдельный контракт)
  • .enum.ts — enum
  • .dto.ts — внешние DTO
  • .schema.ts — схемы валидации
  • .constant.ts — константы
  • .config.ts — конфигурация

Утилиты и хелперы

  • .util.ts — утилиты
  • .helper.ts — вспомогательные функции
  • .lib.ts — вспомогательные функции

Тесты

  • .test.ts / .test.tsx
  • .mock.ts

Хорошо

src/
├── screens/
│   └── main/
│       ├── main.screen.tsx
│       └── index.ts
├── features/
│   └── auth-by-email/
│       ├── ui/
│       │   └── login-form.ui.tsx
│       ├── auth-by-email.feature.tsx
│       └── index.ts
└── shared/
    └── ui/
        └── icon/
            ├── icon.ui.tsx
            └── icon.module.css

Плохо

// Плохо: нет единых правил для слоёв и публичных файлов.
src/
├── screens/
│   └── Main/
│       └── Main.tsx
└── features/
    └── authByEmail/
        └── login-form.tsx

Булевы значения

  • Использовать префиксы is, has, can, should.

Хорошо

const isReady = true;
const hasAccess = false;
const canSubmit = true;
const shouldRedirect = false;

Плохо

// Плохо: неясное булево значение без префикса.
const ready = true;
const access = false;
const submit = true;

События и обработчики

  • Обработчики начинать с handle.
  • События и колбэки начинать с on.

Хорошо

const handleSubmit = () => { ... };
const onSubmit = () => { ... };

Плохо

// Плохо: неочевидное назначение имени.
const submitClick = () => { ... };

Коллекции

  • Для массивов использовать имена во множественном числе.
  • Для словарей/мап — использовать суффиксы ById, Map, Dict.

Хорошо

const users = [];
const usersById = {} as Record<string, User>;
const userIds = ['u1', 'u2'];
const ordersMap = new Map<string, Order>();
const featureFlagsDict = { beta: true, legacy: false } as Record<string, boolean>;

Плохо

// Плохо: имя не отражает, что это коллекция.
const user = [];
// Плохо: словарь назван как массив.
const usersMap = [];
// Плохо: по имени непонятно, что это словарь.
const users = {} as Record<string, User>;

Документирование

Документирование должно помогать понять назначение сущности, а не дублировать её типы или очевидные детали.

Правила

  • Документировать только назначение функций, компонентов, типов, интерфейсов и enum.
  • Не документировать параметры, возвращаемые значения, типы пропсов и очевидные детали.
  • В интерфейсах, типах и enum описывать только смысл поля или значения.
  • Описание должно быть кратким, информативным и завершаться точкой.

Примеры

Хорошо

/**
 * Список задач пользователя.
 */
export const TodoList = memo(() => { ... });

/**
 * Интерфейс задачи.
 */
export interface TodoItem {
  /** Уникальный идентификатор задачи. */
  id: string;
  /** Текст задачи. */
  text: string;
  /** Статус выполнения задачи. */
  completed: boolean;
}

/**
 * Перечисление фильтров задач.
 */
export enum TodoFilter {
  /** Все задачи. */
  All = 'all',
  /** Только активные задачи. */
  Active = 'active',
  /** Только выполненные задачи. */
  Completed = 'completed',
}

Плохо

// Плохо: дублирование параметров и возвращаемых значений.
/**
 * @param id - идентификатор задачи
 * @returns объект задачи
 */

// Плохо: описание очевидных деталей.
/**
 * id — идентификатор задачи
 * text — текст задачи
 * completed — статус выполнения
 */

Типизация

Типизация обязательна для всех публичных интерфейсов, функций и компонентов. Цель — предсказуемость, безопасность и автодополнение.

Общие правила

  • Указывать типы для параметров компонентов, возвращаемых значений и параметров функций.
  • Предпочитать type для описания сущностей и interface для расширяемых контрактов.
  • Избегать any и unknown без необходимости.
  • Не использовать ts-ignore, кроме крайних случаев с явным комментарием причины.

Типы для компонентов

  • Типизировать параметры и публичный интерфейс компонента.
  • Дефолтные значения описывать явно в коде.

Хорошо

/**
 * Параметры кнопки.
 */
interface IOwnProps extends HTMLAttributes<HTMLDivElement> {
  /** Текст кнопки. */
  label: string;
  /** Обработчик клика по кнопке. */
  onClick: () => void;
}

/**
 * Кнопка с пользовательскими стилями.
 */
export const Button:FC<IOwnProps> = ({ className, label, onClick, ...htmlAttr }) => {
  return (
    <div {...htmlAttr} className={cl(styles.root, className)}>
      button
    </div>
  )
}

Плохо

// Плохо: параметры не типизированы.
export const Button = (props) => (
  <button type="button" onClick={props.onClick}>
    {props.label}
  </button>
);

Функции

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

Хорошо

export const formatPrice = (value: number): string => {
  return `${value} ₽`;
};

Плохо

// Плохо: нет явного возвращаемого типа.
export const formatPrice = (value: number) => {
  return `${value} ₽`;
};

Работа с any/unknown

  • any использовать только для временных заглушек.
  • unknown сужать через проверки перед использованием.

Хорошо

const parse = (value: unknown): string => {
  if (typeof value === 'string') {
    return value;
  }

  return '';
};

Плохо

// Плохо: any отключает проверку типов.
const parse = (value: any) => value;

Структура проекта

Раздел описывает базовую структуру проекта и принципы организации модулей на уровне папок и файлов.

Базовая структура проекта

Хорошо

src/
├── app/                  # Инициализация приложения, роутинг, провайдеры
│   ├── config/           # Конфигурации и константы уровня приложения
│   ├── providers/        # Провайдеры и обёртки приложения
│   ├── routing/          # Конфигурация маршрутов
│   └── index.ts          # Публичный API слоя
├── screens/              # Экраны приложения
│   └── Profile/          # Экран профиля
│       └── ...           # ui/model/index.ts
├── layouts/              # Общие шаблоны и каркасы страниц
│   └── MainLayout/       # Основной layout
│       └── ...           # ui/index.ts
├── widgets/              # Крупные блоки интерфейса
│   └── Header/           # Виджет шапки
│       └── ...           # ui/index.ts
├── features/             # Пользовательские сценарии
│   └── auth-by-email/    # Авторизация по email
│       └── ...           # ui/model/api/index.ts
├── entities/             # Бизнес-сущности
│   └── user/             # Сущность пользователя
│       └── ...           # ui/model/api/lib/index.ts
└── shared/               # Общие ресурсы проекта
    ├── ui/               # Базовые UI-компоненты
    ├── lib/              # Утилиты и хелперы
    ├── services/         # Общие сервисы и клиенты
    ├── config/           # Общие конфигурации
    ├── styles/           # Глобальные стили и токены
    └── assets/           # Ресурсы
        ├── images/       # Изображения
        ├── icons/        # Иконки
        ├── fonts/        # Шрифты
        └── video/        # Видео

Плохо

// Плохо: слои смешаны, нет понятных границ и публичного API.
src/
├── components/
├── api/
├── styles/
└── user.ts

Правила организации

  • Каждый слой и модуль хранится в собственной папке.
  • Внутренние реализации разделяются на ui, model, lib, api.
  • Публичный API модуля объявляется в index.ts.
  • Внутренние файлы не импортируются напрямую извне.
  • Не смешивать ответственность разных слоёв в одном модуле.

Компоненты

Раздел описывает правила создания UIкомпонентов. Эти правила обязательны для всех слоёв FSD: app, screens, layouts, widgets, features, entities, shared.

Базовая структура компонента

Минимальный набор файлов: компонент, стили, типы и публичный экспорт.

container/
├── styles/
│   └── container.module.scss
├── types/
│   └── container.interface.ts
├── container.ui.tsx
└── index.ts

Пример базового компонента

styles/container.module.scss

.root {}

В CSS Modules использование имени класса .root — это общепринятое соглашение (best practice)

types/container.interface.ts

import type { HTMLAttributes } from 'react'

/**
 * Параметры контейнера.
 */
export interface ContainerProps extends HTMLAttributes<HTMLDivElement> {}

Интерфес параметров компонента всегда наследует свойства своего тега: div, button, итд..

container.ui.tsx

import type { FC } from 'react'
import { cl } from 'clsx'
import type { ContainerProps } from './types/container.interface'
import styles from './styles/container.module.scss'

/**
 * Контейнер с адаптивной максимальной шириной.
 *
 * Используется для:
 *  - ограничения ширины контента
 *  - центрирования содержимого
 *  - построения адаптивной сетки страницы
 */
export const Container: FC<ContainerProps> = ({ className, ...htmlAttr }) => {
  return (
    <div {...htmlAttr} className={cl(styles.root, className)}>
      Container...
    </div>
  )
}
  • Компонент объявляется через const и экспортируется именованно.
  • Пропсы деструктурируются в сигнатуре; если их больше двух — деструктуризацию переносим в тело компонента.
  • Из пропсов отдельно извлекаются className и ...htmlAttr, чтобы корректно объединять классы и прокидывать остальные атрибуты.
  • cl — короткое имя функции для конкатенации CSSклассов.
  • FC<> используется для декларации children.

index.ts

export { Container } from './container.ui'

Вложенные (дочерние) компоненты

Если для реализации функционала нужны компоненты, которые используются только внутри текущего компонента, создавайте их как вложенные в папке ui/. Такие компоненты не экспортируются наружу и используются только локально.

Вложенные компоненты подчиняются тем же правилам по структуре, именованию и стилю (включая папку styles/ для их стилей).