62 KiB
name, description
| name | description |
|---|---|
| style-guide | Используй при создании, изменении, форматировании или ревью frontend-кода для соблюдения code style и style guide. Триггеры: JS, TS, JSX, TSX, HTML, CSS, .js, .ts, .jsx, .tsx, .module.css, React-компонент, props, стили, CSS Modules, JSDoc, документация, именование, импорты/экспорты, типизация, отформатировать по гайду, поправить кодстайл. НЕ используй для архитектуры SLM, выбора слоя/модуля/public API, генерации .templates, Next.js routing/data fetching/rendering, настройки PostCSS/tooling, backend-кода и вопросов без правки frontend-кода. |
Style guide
База style guide
Применяй этот канон ко всем файлам перед языковыми правилами. Если правило языка уточняет базовое правило, используй языковое правило.
Порядок применения
- Сначала проверь formatter, linter,
editorconfig,tsconfigи локальный стиль затронутых файлов. - Если formatter или linter задаёт формат, следуй ему даже при отличии от этого канона.
- Не меняй formatter, linter или
tsconfigв задаче на обычную правку кода. - Меняй стиль только в затронутом коде. Не форматируй весь проект без отдельной задачи.
- Сохраняй локальную консистентность, если она не конфликтует с автоматическими проверками.
- Не переименовывай публичные сущности без отдельной задачи на переименование.
Базовый формат
- Используй 2 пробела. Не используй табы.
- Ориентируйся на 120 символов в строке, если проект не задаёт другой
lineWidth. - Не переноси читаемую строку механически только ради лимита.
- Переноси выражение на новые строки, когда строка становится плохо читаемой.
- Не переноси строку внутри строкового литерала без необходимости.
Именование
Следуй этим форматам, если проект не задаёт другой локальный стандарт.
| Сущность | Формат |
|---|---|
| Папки и файлы | kebab-case |
| Переменные и функции | camelCase |
| Классы, типы, React-компоненты | PascalCase |
| Константы верхнего уровня | SCREAMING_SNAKE_CASE |
| Хуки | useSomething |
| CSS-классы в CSS Modules | camelCase |
| Enum-ключи | SCREAMING_SNAKE_CASE |
Файлы
- Суффикс нужен, чтобы быстро определить роль или тип открытого файла без контекста дерева папок.
- Суффикс пишется в единственном числе.
- Формат ролевого файла:
name.<suffix>.tsилиname.<suffix>.tsx. - Список ниже - примеры типовых суффиксов, а не закрытый перечень.
- Если роли файла нет в списке, добавь понятный суффикс по тому же принципу и сохраняй локальную консистентность проекта.
- Не переименовывай публичные файлы без отдельной задачи на переименование.
Типовые суффиксы:
use-name.hook.ts- файл хука, функция именуетсяuseName..store.ts- store..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- моки.
Общие правила комментариев
- Комментарий объясняет назначение, сценарий применения, ограничение или неочевидную причину.
- Не добавляй комментарий, который дословно пересказывает код.
- Однострочный комментарий отделяй пробелом после маркера.
- Завершай комментарий точкой, если это фраза или предложение.
- Заголовок комментария пиши по правилам русского языка: с заглавной только первая буква, кроме имён собственных и аббревиатур.
- Если комментарий состоит из заголовка и тела, отделяй заголовок от тела одной пустой строкой.
- Логические части внутри многострочного комментария отделяй одной пустой строкой.
- Удаляй устаревший комментарий при изменении поведения кода.
Чеклист базы
- Formatter, linter,
editorconfig,tsconfigи локальный стиль файла соблюдены. - Diff не содержит форматирования незатронутого кода.
- Отступы, длина строк и переносы не создают альтернативный стиль.
- Имена соответствуют типу сущности и локальному стандарту проекта.
- Комментарии объясняют причину или назначение, а не пересказывают код.
JS/TS
Применяй этот раздел к .js, .ts и JS/TS-частям .jsx и .tsx. Для React-компонентов дополнительно применяй раздел React и TSX, для разметки - раздел HTML, JSX и TSX.
Строки
- Для обычных строк используй одинарные кавычки.
- Строки в обратных кавычках используй только для интерполяции
${...}или многострочного текста.
const label = 'Сохранить';
const title = `Привет, ${name}`;
Импорты и экспорты
- В именованных импортах ставь пробелы внутри фигурных скобок:
import { User } from './user'. - Используй
import type, если импорт нужен только на уровне типов. - Для собственного кода предпочитай именованные экспорты.
default exportиспользуй только когда это требует framework, tooling или устоявшийся локальный стандарт.default importдопустим для сторонних библиотек, CSS Modules и framework API.- Избегай namespace imports вида
import * as api, если библиотека или локальный стандарт этого не требует. - Не импортируй глубже публичного API модуля, если проектная архитектура это запрещает.
- Не меняй направление импортов ради удобства. Для архитектурных ограничений используй
slm-design. - Не меняй порядок, путь или тип импортов только ради форматирования, кроме автоформатирования существующим formatter/linter.
- Не создавай новый barrel или public API без понимания архитектурной границы.
- Экспорт типов оформляй через
export type, если экспортируется только тип.
import { createUser } from './create-user';
import type { User } from './user.type';
export { UserCard } from './user-card';
export type { UserCardProps } from './types/user-card-props.type';
Объекты, массивы, коллекции и вызовы
- Короткие объекты, массивы и вызовы оставляй в одну строку, если они читаются.
- В многострочном объекте размещай каждое свойство на новой строке.
- В многострочном массиве размещай каждый элемент на новой строке.
- В длинном вызове функции размещай каждый аргумент или смысловую группу на новой строке.
- В однострочных объектах и массивах ставь пробелы после запятых.
- Массивы называй во множественном числе.
- Списки идентификаторов называй с суффиксом
Ids. - Словари и мапы называй с суффиксом
ById,MapилиDict.
const roles = ['admin', 'editor', 'viewer']
const options = { id: 1, name: 'User' }
const userIds = ['u1', 'u2']
const usersById = {} as Record<string, User>
const config = createRequestConfig(
endpoint,
{
headers: {
'X-Request-Id': requestId,
'X-User-Id': userId
},
params: {
page,
pageSize,
sort: 'createdAt'
}
},
timeoutMs
)
Точки с запятой и trailing comma
- По умолчанию не ставь точку с запятой в конце инструкций.
- По умолчанию не ставь trailing comma после последнего свойства объекта, элемента массива, аргумента или параметра.
- Если formatter, linter или локальный стиль файла задаёт другое поведение, следуй ему.
- Не делай отдельную правку только ради массового добавления или удаления точек с запятой и trailing comma вне затронутого кода.
Early return
- Используй ранние возвраты для упрощения чтения.
- Избегай
elseпослеreturn.
const getName = (user?: { name: string }) => {
if (!user) {
return 'Гость';
}
return user.name;
};
Булевые значения
- Булевые значения начинай с
is,has,canилиshould. - Не используй нейтральные имена вроде
ready,access,submit, если по имени неясно, что это boolean.
const isReady = true;
const hasAccess = false;
const canSubmit = true;
const shouldRedirect = false;
События и callback
- Обработчики внутри кода называй через
handle*. - Callback-параметры и callback-свойства называй через
on*.
const handleSubmit = () => {
// ...
};
type FormOptions = {
onSubmit: () => void;
};
TypeScript
- Указывай типы для параметров функций и компонентов.
- Для публичных функций указывай возвращаемый тип.
- Не полагайся на неявный вывод типов для публичных API и важных контрактов.
- Предпочитай
typeдля описания сущностей иinterfaceдля расширяемых контрактов. - Избегай
anyиunknownбез необходимости. - Не используй
// @ts-ignore, кроме крайних случаев с явным комментарием причины. - Если нужно временно подавить ошибку TypeScript, предпочитай
// @ts-expect-errorс объяснением причины.
/**
* Форматирует цену с символом валюты.
*/
export const formatPrice = (value: number): string => {
return `${value} ₽`;
};
Type vs interface
- Используй
typeдля props, DTO, view-model, unions, mapped types и композиции типов. - Используй
interface, когда контракт должен расширяться черезextendsили declaration merging. - Не смешивай
typeиinterfaceв одной области без причины.
export type UserCardProps = {
user: User;
onSelect: (userId: string) => void;
};
export interface StorageAdapter {
get(key: string): Promise<string | null>;
set(key: string, value: string): Promise<void>;
}
Any и unknown
anyиспользуй только как временную заглушку или при работе с внешним API, который невозможно типизировать сразу.- Каждый
anyдолжен иметь понятную причину и план замены, если это не неизбежная граница интеграции. unknownиспользуй на границах с внешними данными и обязательно сужай перед использованием.
const parseName = (value: unknown): string => {
if (typeof value === 'string') {
return value;
}
return '';
};
Плохо:
const parseName = (value: any) => value.trim();
Predicates и narrowing
- Для runtime-проверок базовых значений используй
shared/lib/value-predicates. - Если в проекте нет
shared/lib/value-predicates, создай библиотеку перед использованием predicates. Состав утилит, сигнатуры и поведение бери изreference/value-predicatesвнутри skill. - Импортируй predicates из публичного API библиотеки, а не из внутренних файлов.
shared/lib/value-predicatesсодержит только базовые проверки nullish, primitives, strings, arrays, objects и literal unions.- Доменные guards вроде
isOrder,isCity,isUserErrorразмещай рядом с владельцем данных: business-модулем, mapper/source или infra-адаптером. - Не размещай доменные guards в
shared/lib/value-predicates.
Минимальная структура библиотеки:
shared/lib/value-predicates/
├── index.ts
└── value-predicates.ts
import { hasOwn, isArrayOf, isNonEmptyArray, isRecord, isString } from 'shared/lib/value-predicates'
Predicates применяй, когда нужно проверить:
- значение определено или отсутствует;
- значение является
string,numberилиboolean; - строка является непустой;
- массив пустой или непустой;
unknown-значение является массивом элементов нужной формы;unknown-значение является объектом-записью;- значение входит в список допустимых литералов.
Для определения пустого или непустого списка не используй прямые проверки длины массива:
items?.length
items.length > 0
items.length !== 0
items.length === 0
!items.length
Используй predicates:
isEmptyArray(items)
isNonEmptyArray(items)
Для unknown-данных и внешних API-ответов не используй Array.isArray(value) как проверку формы элементов. Используй isArrayOf с item guard:
if (isArrayOf(value, isOrder)) {
// value: Order[]
}
Для object-like unknown используй isRecord и hasOwn перед чтением полей:
const isOrder = (value: unknown): value is Order => {
return isRecord(value) && hasOwn(value, 'id') && isString(value.id)
}
Для literal union используй isOneOf:
const statuses = ['draft', 'published'] as const
if (isOneOf(value, statuses)) {
// value: 'draft' | 'published'
}
Прямое обращение к .length допустимо, когда нужен именно числовой размер:
const count = items.length
if (password.length < 8) {
return 'Минимум 8 символов'
}
for (let index = 0; index < items.length; index += 1) {
// ...
}
Прямой map допустим, если массив заранее нормализован:
const items = data ?? []
return items.map((item) => mapItem(item))
Type assertions
- Не используй
asдля обхода TypeScript без проверки данных. - Допускай
as constдля литеральных конфигураций и enum-like объектов. - Type assertion на границе внешних данных должен сопровождаться проверкой, схемой валидации или явной причиной.
const sortDirections = ['asc', 'desc'] as const;
export type SortDirection = (typeof sortDirections)[number];
Публичный API
- Если файл является public API, экспортируй только то, что разрешено наружу.
- Не экспортируй внутренние helpers, временные типы и implementation details.
- Если задача требует изменить public API слоя или модуля, сначала применяй
slm-design.
Документирование JS/TS
Базовые правила
- Документируй точку объявления именованной JS/TS-сущности.
- JSDoc ставь непосредственно перед объявлением функции, класса, типа, интерфейса, enum или документируемой константы.
- Если функция объявлена через
const name = () => {}, комментарий ставь передconst. - Документируй назначение сущности, а не синтаксис её объявления.
- Объясняй, зачем нужна сущность, какой сценарий она закрывает или какое ограничение важно знать.
- Не документируй использование сущности, вызовы функций и строки внутри тела кода.
- Не дублируй TypeScript-сигнатуру.
- Не описывай параметры, возвращаемые значения и props через
@param,@returnsили@type, если они уже видны из типов. - Если назначение очевидно из имени и типа, всё равно добавь короткий JSDoc без пересказа сигнатуры.
- React-компоненты документируй по правилам раздела
React и TSX.
Базовый шаблон многострочного комментария
- Для документирования JS/TS-сущностей используй многострочный JSDoc-комментарий.
/**
* <Что делает сущность в 1 строке>.
*
* <Опционально: описание сложной механики или важных нюансов>.
*/
Функции
- Каждая именованная функция должна иметь многострочный JSDoc-комментарий.
- Под правило попадают
function name(),const name = () => {},const name = function () {}и именованные обработчики вродеconst handleSubmit = () => {}. - Комментарий функции описывает действие, результат, побочный эффект или важное ограничение.
/**
* Рекурсивно собирает дерево категорий из плоского списка.
*
* Группирует элементы по parentId, начиная с корневых категорий.
* Категории без родителя попадают в корень дерева.
*/
export const buildCategoryTree = (categories: Category[]): CategoryTree[] => {
// ...
}
Классы
- Каждый класс должен иметь многострочный JSDoc-комментарий.
- Комментарий класса описывает роль класса и границу ответственности.
- Не перечисляй методы класса, если их назначение понятно из public API.
/**
* Клиент для работы с заказами через REST API.
*
* Инкапсулирует маршруты, сериализацию параметров и обработку ответа.
*/
export class OrdersClient {
// ...
}
Типы и интерфейсы
- Каждый
typeиinterfaceдолжен иметь многострочный JSDoc-комментарий. - Комментарий типа или интерфейса описывает контракт: модель, DTO, props, config, состояние или внешний формат.
- Каждое поле
typeилиinterfaceдолжно иметь JSDoc-комментарий/** ... */непосредственно над полем. - Комментарий поля описывает смысл значения, доменное ограничение, формат или сценарий использования.
- Не объединяй одним комментарием несколько полей.
- Не отделяй поля пустой строкой только из-за JSDoc-комментария.
- Пустую строку между полями добавляй только для смыслового разделения групп полей.
- Если поле кажется очевидным, всё равно добавь короткое описание без пересказа имени.
/**
* Фильтры списка заказов.
*/
export type OrderFilters = {
/** Идентификатор пользователя-владельца заказов. */
userId?: string
/** Статус заказа для фильтрации выдачи. */
status?: OrderStatus
}
Константы
- Документируй константу только если она является публичным контрактом, доменным ограничением, magic value или переиспользуемой конфигурацией.
- Обычные локальные константы не документируй.
/**
* Максимальное количество заказов на одной странице выдачи.
*
* Значение синхронизировано с ограничением backend API.
*/
export const MAX_ORDERS_PAGE_SIZE = 100
Enum
- Каждый
enumдолжен иметь многострочный JSDoc-комментарий. - Комментарий enum описывает назначение набора значений.
- Каждое значение enum должно иметь JSDoc-комментарий
/** ... */непосредственно над значением. - Комментарий значения enum описывает смысл значения или состояние, которое оно обозначает.
- Не отделяй значения enum пустой строкой только из-за JSDoc-комментария.
- Не оставляй значение enum без комментария, даже если оно кажется очевидным.
/**
* Состояние оплаты заказа.
*/
export enum PaymentStatus {
/** Оплата создана, но ещё не подтверждена провайдером. */
PENDING = 'pending',
/** Оплата успешно подтверждена провайдером. */
PAID = 'paid',
/** Оплата отклонена или завершилась ошибкой. */
FAILED = 'failed'
}
Что не документировать
- Не документируй обычные переменные и inline callback внутри вызовов вроде
useEffect,map,filter,reduce,forEach,thenили обработчиков API. - Если inline callback требует пояснения, вынеси его в именованную функцию и задокументируй объявление.
Плохо
/**
* @param value - число.
* @returns строка с ценой.
*/
export const formatPrice = (value: number): string => {
return `${value} ₽`
}
Чеклист JS/TS
- Строки, объекты, массивы, коллекции, вызовы, semicolon и trailing comma соответствуют дефолту или formatter/linter.
- Early return упрощает чтение и не меняет поведение.
- Булевые значения и callback названы по JS/TS-соглашениям.
- Параметры функций типизированы; публичные функции имеют возвращаемый тип.
any,unknown,asи suppress-комментарии имеют явную причину.- Если
shared/lib/value-predicatesотсутствует, библиотека создана поreference/value-predicatesперед использованием predicates. - Проверки пустых и непустых массивов используют
shared/lib/value-predicates, а не прямые.length-условия. unknownмассивы проверяются черезisArrayOf(value, itemGuard), если важен тип элементов.- Object-like
unknownпроверяется черезisRecordиhasOwnперед чтением полей. - Доменные guards размещены рядом с владельцем данных, а не в
shared/lib/value-predicates. - Импорты типов оформлены через
import type, а экспорты типов черезexport type. - Public API не раскрывает implementation details.
- JS/TS-функции, классы, типы, интерфейсы, поля типов, enum, значения enum и константы-контракты документированы по правилам своего подраздела.
React и TSX
Этот раздел описывает прикладную форму React UI-сущности после того, как архитектура уже определила её место: компонент внутри ui/ или UI-модуль с корневым .tsx. Архитектурные границы компонентов, модулей, сегментов и public API определяет slm-design. Форму JSX-разметки дополнительно проверяй по разделу HTML, JSX и TSX.
Базовая React UI-сущность
- Используй эту форму для базового React-компонента или UI-модуля с корневым компонентом.
- Держи
.tsx, props-типы, CSS Module и локальныйindex.tsв отдельных файлах. - Не объявляй props-типы внутри
.tsx. - Не размещай CSS рядом с TSX-кодом: стили живут в
styles/{name}.module.css. - Корневой CSS-класс всегда называется
.root. - Компонент обязан иметь JSDoc-комментарий по React-шаблону.
- Всё, что не является React-компонентом, документируй по правилам
Документирование JS/TS.
Структура файлов
- Базовая React UI-сущность живёт в собственной папке.
- Файлы структуры обязательны для новой сущности:
.tsx,types/,styles/,index.ts. - Имя файла props-типа строится как
{name}-props.type.ts. - CSS Module строится как
{name}.module.css. index.tsэкспортирует компонент и public props-тип.
user-status/
├── styles/
│ └── user-status.module.css
├── types/
│ └── user-status-props.type.ts
├── user-status.tsx
└── index.ts
Типизация props
- Props-типы выноси в
types/{name}-props.type.ts. - Собственные параметры компонента называй
{Name}Params. - Атрибуты корневого HTML-элемента называй
RootAttrs. - Итоговый тип props называй
{Name}Props. RootAttrsтипизируй черезComponentPropsWithoutRef<'tag'>.- Исключай из
RootAttrsатрибуты, которыми компонент управляет сам: напримерchildren, если компонент рендерит собственный контент. - Если собственный параметр конфликтует с HTML-атрибутом, исключай этот атрибут через
Omit. - Документируй
Params,RootAttrs,Propsи каждое поле props по правиламJS/TS.
import type { ComponentPropsWithoutRef } from 'react'
/**
* Собственные параметры UserStatus.
*/
export type UserStatusParams = {
/** Текст статуса пользователя. */
label: string
/** Доступен ли пользователь сейчас. */
isOnline: boolean
}
/**
* Атрибуты корневого span без children.
*/
type RootAttrs = Omit<ComponentPropsWithoutRef<'span'>, 'children'>
/**
* Props UserStatus.
*/
export type UserStatusProps = RootAttrs & UserStatusParams
Реализация TSX
- В
.tsxдержи сам компонент, импорт props-типа, импорт CSS Module и импорт функции склейки классов. - Функцию склейки классов импортируй как
cl:import cl from 'clsx'. - Компонент объявляй через
constи именованный экспорт, если framework или локальный стандарт не требует другой формы. - Не используй
React.FCпо умолчанию. - Параметр компонента называй
propsи типизируй через{Name}Props. - Возвращаемый тип компонента не указывай: TypeScript корректно выводит JSX-результат, а явный
ReactElementсужает допустимые варианты возврата. - Деструктурируй props внутри тела компонента, а не в сигнатуре.
- Из props обязательно выделяй
classNameи...rootAttrs, если корневой элемент принимает HTML-атрибуты. - Прокидывай
rootAttrsна корневой DOM-элемент. - Корневой элемент обязан получить
styles.rootпервым CSS-классом. - Внешний
classNameдобавляй последним аргументом вcl(...). - Не создавай вложенный компонент внутри render без причины.
- Не дублируй условия в JSX, если их можно выразить через заранее подготовленную переменную с понятным именем.
- Для сложной разметки сначала упрощай данные и условия, потом JSX.
import cl from 'clsx'
import type { UserStatusProps } from './types/user-status-props.type'
import styles from './styles/user-status.module.css'
/**
* Статус пользователя в карточке профиля.
*
* Используется для:
* - отображения текущей доступности пользователя
* - визуального выделения онлайн- и офлайн-состояний
*/
export const UserStatus = (props: UserStatusProps) => {
const { label, isOnline, className, ...rootAttrs } = props
return (
<span
{...rootAttrs}
className={cl(styles.root, isOnline && styles.online, className)}
>
{label}
</span>
)
}
Props и events
- Callback props называй через
on*:onSubmit,onChange,onClose. - Внутренние обработчики называй через
handle*:handleSubmit,handleChange,handleClose. - Не прокидывай событие DOM наружу, если внешний код должен знать только бизнес-смысл действия.
- Boolean props называй через
is*,has*,can*илиshould*. - Named handlers документируй как функции по правилам
JS/TS. - Inline callback в JSX props не документируй.
Hooks
- Соблюдай Rules of Hooks: вызывай hooks только на верхнем уровне компонента или другого hook.
- Не используй
useEffectдля вычисления данных, которые можно получить из props/state во время render. - Выноси повторяемую stateful-логику в custom hook только когда она реально переиспользуется или упрощает компонент.
- Custom hook называй через
use*. - Вызовы
useEffect,useMemo,useCallbackи других hooks не документируй. - Inline callback внутри hooks не документируй.
- Если логика hook требует комментария, вынеси её в именованную функцию или custom hook и документируй объявление по
JS/TS. - Custom hook документируй как именованную функцию по
JS/TS.
Стили
- CSS Module размещай в
styles/{name}.module.css. - Корневой CSS-класс всегда называй
.root. .rootописывает реальный корневой DOM-элемент компонента.- Дополнительные классы описывай рядом с
.rootи подключай черезstyles.*. - Не подменяй
.rootвнешнимclassName: внешний класс только дополняетstyles.root.
.root {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--color-text-muted);
}
Локальный экспорт
- В
index.tsэкспортируй компонент и public props-тип. - Компонент экспортируй обычным named export.
- Props экспортируй через
export type. - Не экспортируй
ParamsиRootAttrsнаружу без необходимости.
export { UserStatus } from './user-status'
export type { UserStatusProps } from './types/user-status-props.type'
Документирование компонентов
- Каждый React-компонент должен иметь многострочный JSDoc-комментарий.
- Комментарий ставь непосредственно перед объявлением компонента.
- Компонент документируй по React-шаблону, а не по шаблону функции из
JS/TS. - Компонент описывает назначение и сценарии применения.
- Комментарий должен помогать понять, когда использовать компонент, без чтения реализации.
- В
Используется дляуказывай только реальные сценарии применения. - Добавляй минимум один сценарий; верхнего ограничения по количеству сценариев нет.
- Не добавляй сценарии ради количества.
- Не описывай реализацию вида
рендерит div с className, если это видно из кода. - Всё, что не является React-компонентом, документируй по правилам
Документирование JS/TS. - Props-типы, типы, enum, классы, функции, константы-контракты и named handlers подчиняются правилам
JS/TS. - Вызовы
useEffect,useMemo,useCallbackи других hooks не документируй. - Inline callback внутри hooks,
map,filter, event props и других вызовов не документируй. - Если inline callback требует пояснения, вынеси его в именованную функцию и задокументируй объявление по
JS/TS.
Шаблон:
/**
* <Назначение компонента в 1 строке>.
*
* Используется для:
* - <сценарий 1>
* - <сценарий 2, если есть>
*/
Плохо:
/**
* Рендерит span с className и label.
*/
export const UserStatus = (props: UserStatusProps) => {
// ...
}
Чеклист React/TSX
- Базовая React UI-сущность имеет
.tsx,types/,styles/иindex.ts. - Props вынесены в
types/{name}-props.type.tsи собраны из{Name}Params,RootAttrs,{Name}Props. - Props-типы и поля props документированы по правилам
JS/TS. - Компонент объявлен через
const, принимаетpropsи деструктурирует их внутри тела. - Компонент не использует
React.FCпо умолчанию и не указывает возвращаемый тип без необходимости. - Корневой DOM-элемент получает
{...rootAttrs}иclassName={cl(styles.root, ..., className)}. - CSS Module лежит в
styles/{name}.module.css, корневой класс называется.root. index.tsэкспортирует компонент и props-тип.- Каждый React-компонент имеет комментарий по React-шаблону с назначением и сценариями применения.
- Callback props названы через
on*, внутренние обработчики - черезhandle*. - Named handlers и custom hooks документированы по правилам
JS/TS; вызовы hooks и inline callbacks не документируются. - Hooks вызваны только на верхнем уровне компонента или custom hook.
HTML, JSX и TSX
Применяй этот раздел к HTML-разметке и JSX/TSX-деревьям. Для React-компонентов дополнительно применяй раздел React и TSX, для JS/TS-выражений внутри JSX/TSX - раздел JS/TS.
Атрибуты и props
- HTML-теги и HTML-атрибуты пиши в нижнем регистре.
- Строковые значения HTML/JSX/TSX-атрибутов пиши в двойных кавычках.
- Динамические значения передавай через
{...}. - Статические boolean-атрибуты записывай без
={true}. - Динамические boolean-атрибуты записывай через выражение:
disabled={isSaving}. - Не добавляй пустые, дублирующиеся или неиспользуемые атрибуты.
- Props компонентов именуй по правилам
React и TSX.
<button
type="button"
disabled={isSaving}
onClick={handleSave}
>
Сохранить
</button>
Формат элемента
- Короткий элемент с небольшим числом атрибутов или props можно оставлять в одну строку.
- Если атрибутов или props много, размещай каждый на отдельной строке.
- Закрывающую скобку многострочного элемента размещай на отдельной строке.
- Если элемент содержит вложенные элементы, размещай открывающий и закрывающий теги на отдельных строках.
- Не смешивай в одном участке разметки разные стили записи props без причины.
<input type="text" name="email" />
<UserCard
user={user}
isSelected={isSelected}
onSelect={handleSelect}
/>
Условия и значения в разметке
- Не усложняй JSX/TSX условиями, которые можно подготовить в переменной до
return. - Не размещай составные runtime-условия непосредственно в JSX/TSX.
- Не используй тернарные выражения внутри JSX/TSX-разметки.
- Подготовь label, локальные данные, boolean-флаги, списки и ветки отображения до JSX.
- В JSX используй простые predicate gates или заранее подготовленные флаги.
- Если для JSX-условий нужны predicates, но в проекте нет
shared/lib/value-predicates, сначала создай библиотеку поreference/value-predicates. - Если ветвление большое, вынеси его в переменную, named function или отдельный компонент по правилам
React и TSX. - Render-переменные вида
profileContent,ordersContent,emptyStateContentиспользуй только когда ветка большая, повторяется или содержит несколько состояний. - Если условие простое, не создавай render-переменную без необходимости.
- Не дублируй длинные пути к данным внутри JSX: вынеси значение в локальную переменную до
return. - Inline callback в JSX props не документируй; если ему нужно пояснение, вынеси в named handler.
const submitLabel = isSaving ? 'Сохраняем' : 'Сохранить'
const profileUser = currentUser.error ? null : currentUser.data
const ordersData = orders.data
const shouldShowEmptyState = isEmptyArray(ordersData) && !orders.isLoading
return (
<>
<button type="submit" disabled={isSaving}>
{submitLabel}
</button>
{isDefined(profileUser) && (
<Profile user={profileUser} />
)}
{isNonEmptyArray(ordersData) && ordersData.map((order) => (
<OrderCard key={order.id} order={order} />
))}
{shouldShowEmptyState && (
<EmptyState />
)}
</>
)
Плохо:
return (
<>
<button type="submit">
{isSaving ? 'Сохраняем' : 'Сохранить'}
</button>
{data && !currentUser.error && (
<Profile user={data} />
)}
{orders.data?.length !== 0 && !orders.isLoading && orders.data?.map((order) => (
<OrderCard key={order.id} order={order} />
))}
</>
)
Для conditional rendering списков не используй прямые проверки длины массива:
items?.length
items.length > 0
items.length !== 0
items.length === 0
!items.length
items && items.map(...)
Для уже типизированных массивов используй isEmptyArray и isNonEmptyArray:
const ordersData = orders.data
{isNonEmptyArray(ordersData) && (
<OrdersList orders={ordersData} />
)}
{isEmptyArray(ordersData) && !orders.isLoading && (
<EmptyState />
)}
Для unknown или внешних API-данных используй isArrayOf с item guard:
const ordersData = response.data
{isArrayOf(ordersData, isOrder) && (
<OrdersList orders={ordersData} />
)}
Перед проверкой выноси список в локальную переменную. Не дублируй длинный путь к данным внутри JSX:
const itemsData = query.data
{isNonEmptyArray(itemsData) && itemsData.map((item) => (
<Card key={item.id} item={item} />
))}
Render-переменные используй только для крупных или повторяющихся веток:
const ordersContent = isNonEmptyArray(ordersData) ? (
<OrdersList orders={ordersData} />
) : null
return (
<>
{ordersContent}
</>
)
Для подготовленных boolean-значений используй смысловые префиксы:
is*- состояние или классификация:isLoading,isProfileReady.has*- наличие значения или содержимого:hasOrders,hasError.can*- возможность действия:canSubmit,canEditProfile.should*- UI-решение:shouldShowEmptyState,shouldDisableSubmit.
Для локальных данных используй имена с суффиксами Data, Items, List:
const ordersData = orders.data
const cityItems = citySuggestions.data
Wrapper-элементы и семантика
- Не добавляй wrapper-элемент только ради форматирования.
- Не добавляй wrapper, если он меняет DOM-структуру, CSS-поведение или доступность.
- Не меняй HTML-семантику или ARIA в задаче на чистое форматирование.
- Если задача требует выбрать семантический тег, ARIA или поведение формы, решай это как отдельную semantic/accessibility-задачу, а не как style-only правку.
return (
<section aria-labelledby={titleId}>
<h2 id={titleId}>{title}</h2>
{children}
</section>
)
Комментарии
- В HTML/JSX/TSX-разметке по умолчанию обходись без комментариев.
- Комментируй только хаки, внешние ограничения и неочевидные причины.
- JSX/TSX-комментарий оформляй только однострочно:
{/* Комментарий. */}. - HTML-комментарий оформляй только однострочно:
<!-- Комментарий. -->. - Не используй многострочные комментарии в HTML/JSX/TSX-разметке.
- Не оставляй комментарий в разметке, если можно дать блоку, компоненту или переменной понятное имя.
- Не комментируй очевидную структуру, семантику или назначение элемента.
Чеклист HTML/JSX/TSX
- Атрибуты и props оформлены единообразно с локальным стилем файла.
- Строковые атрибуты используют двойные кавычки.
- Boolean-атрибуты записаны без лишнего
={true}. - Многострочный элемент читается без горизонтального скролла.
- В JSX/TSX-разметке нет тернарных выражений.
- JSX использует подготовленные локальные данные,
is*,has*,can*,should*или простые predicate gates вместо составных runtime-условий в разметке. - Conditional rendering списков использует
isEmptyArray,isNonEmptyArrayилиisArrayOf, а не прямые.length-условия в JSX. - Длинные пути к данным не дублируются внутри JSX: список выносится в локальную переменную до
return. - Render-переменные не создаются для простых условий без необходимости.
- Нет пустых, дублирующихся или случайных wrapper-элементов.
- Комментарии в HTML/JSX/TSX используются только для хаков, внешних ограничений и неочевидных причин.
CSS
Этот канон описывает, как писать CSS без привязки к frontend-фреймворку, препроцессору или PostCSS-настройке. Он не описывает установку tooling, подключение плагинов и диагностику сборки.
Базовый подход
- По умолчанию стили пишутся модульно: локальный UI-стиль живёт в CSS Module владельца.
- CSS Module принадлежит конкретному компоненту или модулю.
- Не импортируй CSS Module одного компонента или модуля в другой.
- Не выноси локальные классы компонента в global styles.
- Глобальные стили используй только для проектных основ: tokens, media, reset, typography, themes.
- Если стиль должен переиспользоваться, выноси переиспользуемую UI-сущность или token, а не общий CSS Module.
CSS Modules
- Корневой класс CSS Module всегда называй
.root. - Обычные классы внутри CSS Module называй в
camelCase. - Не смешивай
camelCase, BEM иkebab-caseв одном CSS Module. - Внешний
classNameдолжен дополнять.root, а не заменять его. - Модификаторы оформляй отдельными короткими классами с
_:._active,._disabled,._open.
.root {
display: flex;
}
.title {
color: var(--color-text);
}
.button {
opacity: 1;
&._disabled {
opacity: 0.5;
}
}
Форматирование
- Используй 2 пробела. Не используй табы.
- В каждом CSS-правиле размещай одно свойство на строку.
- Между CSS-правилами верхнего уровня оставляй одну пустую строку.
- Перед каждым вложенным блоком оставляй одну пустую строку.
- Если проект не задаёт порядок, группируй свойства так: позиционирование, блочная модель, оформление, текст, прочее.
- Не меняй порядок свойств во всём файле без отдельной задачи на форматирование.
.root {
position: relative;
display: flex;
width: 100%;
padding: var(--space-4);
border-radius: var(--radius-2);
background-color: var(--color-bg);
color: var(--color-text);
}
Вложенность
- Не вкладывай селекторы друг в друга без необходимости.
- Разрешённая вложенность:
@media, псевдоклассы, псевдоэлементы, modifier-классы. - Не вкладывай элементы компонента внутрь
.root; заводи отдельный класс. - Не пиши каскад вида
.root .title, если можно использовать локальный класс.title. - Каждый вложенный блок отделяй пустой строкой от свойств и соседних вложенных блоков.
.root {
display: flex;
color: var(--color-text);
&:hover {
color: var(--color-primary);
}
&::before {
content: '';
}
&._active {
opacity: 1;
}
@media (--md) {
display: grid;
}
}
.title {
color: var(--color-text);
}
Плохо:
.root {
display: flex;
.title {
color: var(--color-text);
}
}
Модификаторы
- Модификатор оформляй отдельным коротким классом с
_:._active,._disabled,._open. - Модификатор описывай через вложенность внутри базового класса:
&._active. - Не кодируй состояние через BEM-цепочки вроде
.button_active. - Не создавай отдельный modifier-файл.
- Не используй модификатор для самостоятельной сущности: если стиль можно назвать отдельным элементом, заведи отдельный класс.
.root {
opacity: 1;
&._disabled {
pointer-events: none;
opacity: 0.5;
}
}
Media queries
- Mobile First обязателен: базовое состояние описывает минимальный viewport без media query.
- Стили для больших viewport добавляй поверх базового состояния через
@media (--*). - Запрещено писать desktop-first: сначала desktop-стили, потом откатывать их вниз через
max-width. - Запрещено игнорировать Mobile First ради удобства конкретного блока.
- По умолчанию используй custom media:
@media (--md),@media (--lg),@media (--h-md). - Локальный
@media (min-width: ...)допустим только как исключение для уникального порога, который нужен одному конкретному компоненту и не подходит для общей media-шкалы. - Локальный
@media (min-width: ...)не отменяет Mobile First: это тоже расширение вверх от базового состояния. - У локального breakpoint обязательно оставляй короткий комментарий с причиной исключения.
- Если breakpoint совпадает с общей шкалой или может понадобиться повторно, используй существующий custom media или добавь новый в
media.css. @mediaпиши внутри селектора, который изменяется.- Не пиши
@mediaверхнего уровня с набором селекторов внутри. - Если шкалы media не хватает, расширяй
media.cssкак проектный стандарт, а не добавляй локальный breakpoint.
.root {
display: grid;
grid-template-columns: 1fr;
@media (--md) {
grid-template-columns: repeat(2, 1fr);
}
/* Уникальный порог, где карточки этого блока помещаются в 3 колонки. */
@media (min-width: 68.75rem) {
grid-template-columns: repeat(3, 1fr);
}
}
Плохо:
@media (--md) {
.root {
display: flex;
}
.title {
font-size: 20px;
}
}
.root {
@media (min-width: 48rem) {
display: flex;
}
}
.card {
display: flex;
/* Плохо: desktop-first откат вниз вместо Mobile First расширения вверх. */
@media (max-width: 47.9375rem) {
display: block;
}
}
media.css
- Все custom media объявляй в отдельном файле
media.css. media.cssсодержит только@custom-media.- Не объявляй custom media внутри CSS Modules.
- Не дублируй breakpoints локально в компонентах.
- Имена ширины пиши короткой шкалой
--xs...--3xl. - Имена высоты пиши с префиксом
--h-. - Значения ширины пиши через
min-width, кроме--xsкак диапазона меньше--sm. - Значения высоты пиши через
min-height. - Используй
remдля значений media; пиксельный эквивалент указывай комментарием.
Базовый список media:
/* Ширина: Mobile First, кроме --xs. */
@custom-media --xs (max-width: 35.9375rem); /* до 575px */
@custom-media --sm (min-width: 36rem); /* 576px */
@custom-media --md (min-width: 48rem); /* 768px */
@custom-media --lg (min-width: 62rem); /* 992px */
@custom-media --xl (min-width: 75rem); /* 1200px */
@custom-media --2xl (min-width: 88rem); /* 1408px */
@custom-media --3xl (min-width: 120rem); /* 1920px */
/* Высота. */
@custom-media --h-xs (min-height: 41.6875rem); /* 667px */
@custom-media --h-sm (min-height: 43.875rem); /* 702px */
@custom-media --h-md (min-height: 50.625rem); /* 810px */
@custom-media --h-lg (min-height: 56.25rem); /* 900px */
@custom-media --h-xl (min-height: 62.5rem); /* 1000px */
@custom-media --h-2xl (min-height: 68.75rem); /* 1100px */
@custom-media --h-3xl (min-height: 75rem); /* 1200px */
Tokens и значения
- Цвета, радиусы, spacing и повторяемые смысловые значения используй через CSS variables.
- Локальное одноразовое значение можно оставить в CSS Module.
- Не выноси значение в token только ради форматирования.
- Не меняй token-систему в задаче на локальную CSS-правку.
- Не дублируй смысловое значение в нескольких CSS Modules, если оно уже является token.
.root {
padding: var(--space-4);
border-radius: var(--radius-2);
background-color: var(--color-bg);
}
Комментарии
- CSS-комментарий оформляй через
/* Комментарий. */. - Комментируй только неочевидные ограничения, hacks и причины.
- Не комментируй очевидные CSS-свойства.
- Удаляй устаревший комментарий при изменении поведения стиля.
Чеклист CSS
- Локальный UI-стиль написан через CSS Module владельца.
- Корневой класс CSS Module называется
.root. - Классы CSS Module названы в
camelCase, модификаторы - через._modifier. - Одно CSS-свойство размещено на строку.
- Между правилами верхнего уровня и перед вложенными блоками есть пустая строка.
- Вложенность используется только для
@media, псевдоклассов, псевдоэлементов и модификаторов. - Mobile First соблюдён: базовое состояние написано без media query, расширения идут вверх через
@media (--*). - Desktop-first и откаты вниз через
max-widthне используются. - Breakpoints из общей шкалы используются через
@media (--*); локальныеmin-widthесть только как обоснованные одноразовые исключения с комментарием причины. - Все custom media объявлены в
media.css. media.cssсодержит только@custom-media.- Повторяемые смысловые значения оформлены через CSS variables.
- CSS-комментарии объясняют неочевидную причину, а не пересказывают свойства.