12 KiB
Frontend UI
Выбор компонента и state
Сначала переиспользуй подходящий component проекта; иначе используй готовый
Mantine component и общую тему. Для выпадающего выбора используй Select или
MultiSelect, а не браузерный <select> / NativeSelect по умолчанию.
Нативный control или собственная реализация допустимы, когда готовые components
не покрывают конкретное продуктовое требование или доступность; укажи причину
в результате работы. Краткость кода и удобство тестирования не являются причиной:
ограничения JSDOM решай в test setup, сохраняя production UI.
Семантический HTML для структуры страницы остаётся уместным.
Wrapper нужен для проектной ответственности, а не ради нового имени для Button. Локальный CSS module принадлежит component; общий token вводи для реально общей роли через design system.
| Состояние | Владелец |
|---|---|
| Ответ сервера / query lifecycle | TanStack Query, data rules |
| Ввод и ошибки формы | React Hook Form + Zod, container/VM |
| Параметр навигации, который должен переживать переход/ссылку | Router/URL |
| Локальное открытие, focus, rendering fallback | React UI state |
| Значение, вычисляемое из уже доступных inputs | Вычисление при render/в VM, без копирования через effect в state |
Effect нужен для синхронизации с внешней системой; действие пользователя обрабатывай в handler. Не добавляй store, memoization или abstraction без текущей потребности. Inspect ближайший component до создания нового pattern.
Container, VM и view
Для data-driven finance page container вызывает один page VM и выбирает Loading/Error/Empty/Loaded view. VM владеет data/auth/route inputs, state selection, retry callbacks, domain grouping и derived presentation data. View получает props.
| Файл | Ответственность |
|---|---|
Component.tsx |
Container или публичный component |
Component.vm.ts |
Page VM, данные и действия |
Component.view.tsx |
Presentation |
Component.types.ts |
Переиспользуемые props/local types, когда отдельный owner полезен |
Component.module.css |
Локальные стили |
Во view нет data/auth/router/navigation/VM hooks и вычислений по domain collection.
Допустимы useTranslation, form-context hooks, UI-only state (например failed
image fallback) и formatter одного переданного значения. Группировка/доли
коллекции готовятся в VM. Перенос fetch во view ради меньшего container нарушает границу.
Отдельный state/section с самостоятельной responsibility выноси в flat
components/*.view.tsx. Небольшой tightly-coupled renderer оставляй рядом.
Не создавай folder/barrel/test/CSS для каждого view и не дроби по line count.
Пример: AccountIncomePage
и его VM.
Формы
Используй react-hook-form, zodResolver, FormProvider. Container/VM создаёт
form state, view получает onSubmit/disabled, fields используют useFormContext.
Во время submit блокируй все действия формы. Zod errors показывай у поля;
network error — отдельным Alert с сохранением ввода. Невалидная форма не отправляет request.
Состояния и recovery
Без успешных данных выбирай loading → error → empty → loaded. При наличии query данных сохраняй их во время refetch и его ошибки: progress, локальное предупреждение, retry. Это Client Cached Snapshot, отдельный от server stale/fallback. Known API errors принадлежат local view; непойманные route errors — route-keyed ErrorBoundary. Сброс query cache после смены session/connection задают data rules.
Initial loading использует page-owned structural skeleton по semantic regions, не одиночный spinner/StateView. Partial loading принадлежит section. Busy-область доступна assistive technology, shapes декоративны, без данных, интерактивности и focus. Route fallback сохраняет shell; до auth ready — neutral shell skeleton. Button loading при mutation и компактный cached-snapshot progress остаются контекстными indicators. Проверяй keyboard access к actions и безопасный retry.
Код и components
Style/type automation задают configs. Используй import type; enum и
namespace в frontend не применяются. Named components — const-valued; Biome
проверяет declarations, no-default-export и exports-last в своём scope.
Обязательный review: FC<Props> и named props рядом/в types-файле; без inline
props typing и destructuring в параметрах. Принимай props, destructure в body.
Только children — FC<PropsWithChildren>; дополнительные props — named type
на основе PropsWithChildren<{ ... }>. Без props — FC.
Своди обычные exports к читаемому нижнему named block; barrel делает прямой
re-export без промежуточного import.
CSF *.stories.tsx сохраняют default meta и прямые story exports для indexer.
Vite/Vitest configs и .storybook/main.ts/preview.tsx имеют только
конфигурационное default-export exception. Scope исключений принадлежит biome.json.
Domain literals and magic values
Finite business vocabulary must be modeled explicitly; incidental implementation literals stay inline. Это относится к error codes, statuses, operation/account/ instrument/transaction types, permissions, roles, feature states и бизнес-режимам.
Перед новым определением найди existing owner/type/value. В branching и comparisons
используй символическое значение владельца; не повторяй raw domain literals.
Храни словарь у domain/module/entity с соблюдением границ слоёв, без глобальной
свалки constants.ts. Сохраняй принятый pattern; для нового project-owned словаря
без подходящего аналога используй as const с derived union:
const BankErrorCode = {
InvalidDate: "BANK_INVALID_DATE",
ImmutableOperation: "BANK_IMMUTABLE_OPERATION",
} as const;
type BankErrorCode = (typeof BankErrorCode)[keyof typeof BankErrorCode];
// Вместо code === "BANK_INVALID_DATE":
if (code === BankErrorCode.InvalidDate) { /* existing handling */ }
Значимые numeric policies (timeout, retry, page size, threshold, coefficient)
называй по смыслу и единицам: REQUEST_TIMEOUT_MS, CACHE_TTL_SECONDS,
MAX_RETRY_COUNT, PAGE_SIZE. Оставляй inline UI labels в принятом месте,
log messages, индексы, index + 1, Math.max(value, 0), page ?? 1,
percentage * 100 и очевидные локальные детали без semantic benefit.
При неясном owner/смысле оставь значение и укажи ограничение в review.
Не мигрируй корректные existing enum/union styles массово. Не добавляй ради правила factories, registries, schemas, loaders, abstraction layers, dependencies, custom lint/AST rules, generation/validation scripts или тесты существования констант. Semantic review проверяет ownership, единое написание и неизменность wire values; обычные behavior tests проверяют поведение.
Backend API vocabulary бери из generated OpenAPI types/client, runtime values —
когда генератор их предоставляет. Не создавай ручную копию backend enum/object.
Текущий schema.d.ts предоставляет только types: используй indexed-access type
из paths; typed literals в запросах/сравнениях допустимы при отсутствии
runtime values. ProblemDetailsDto.code сейчас string: не выдумывай закрытый
frontend error enum. Развитие API source — отдельное contract-решение.
Frontend enum по-прежнему запрещён локальным TS/style profile.
noMagicNumbers включён как error в Biome. Fixtures, tests и stories исключены
из этого правила; остальные проверки для них сохраняются. Используй имена
политик/единиц и existing values. Точечное подавление допустимо для очевидного
incidental значения с объяснением, например перевода доли в проценты; не подавляй
скрытые бизнес-лимиты. Semantic magic strings проверяются conventions/review: в
установленном Biome нет такого встроенного правила, новый инструмент не добавляем.
Строки и валидация
Продуктовый текст хранится в src/shared/i18n/locales/ru.ts, читается через
useTranslation(). В Zod используй validationMessage(key) из i18n/zod, без
пользовательских literal messages. Это mandatory semantic review: общего запрета
непереведённых JSX-строк в Biome/TypeScript нет. Synthetic story fixtures и
описания историй допустимы; они не заменяют продуктовую локализацию.
Проверка
Type/style: npm run typecheck, npm run lint. Для risk behavior/изменения тестов
используй Testing. UI: keyboard, narrow viewport,
loading/error/empty/loaded и recovery по применимости. Storybook
уже установлен; используй его как reference, но сверяй UI с задачей отдельно от build.