From 5874d3604c1f7be98af48da9368176f30b993ae4 Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Mon, 11 May 2026 20:56:41 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D1=8B=20Rea?= =?UTF-8?q?ct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - добавлен раздел примеров React в сайдбар - добавлены примеры создания и композиции фабрик - перенесены подробные React-примеры из раздела модулей - обновлены сгенерированные артефакты документации --- .vitepress/config.ts | 8 + docs/architecture/modules.md | 82 +--- docs/examples/react/composition-provider.md | 249 ++++++++++ docs/examples/react/factory-composition.md | 52 +++ docs/examples/react/factory.md | 114 +++++ public/ARCHITECTURE.md | 490 ++++++++++++++++---- 6 files changed, 836 insertions(+), 159 deletions(-) create mode 100644 docs/examples/react/composition-provider.md create mode 100644 docs/examples/react/factory-composition.md create mode 100644 docs/examples/react/factory.md diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 96e2935..6091c2f 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -11,6 +11,14 @@ const sidebar = [ { text: 'Монорепозитории', link: '/architecture/monorepo' }, ], }, + { + text: 'Примеры React', + items: [ + { text: 'Создание фабрики', link: '/examples/react/factory' }, + { text: 'Композиция фабрик', link: '/examples/react/factory-composition' }, + { text: 'Композиция через Provider', link: '/examples/react/composition-provider' }, + ], + }, ]; export default defineConfig({ diff --git a/docs/architecture/modules.md b/docs/architecture/modules.md index 3edf889..7c5192f 100644 --- a/docs/architecture/modules.md +++ b/docs/architecture/modules.md @@ -194,87 +194,11 @@ Business-модуль всегда экспортирует фабрику. Фа Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция». -### Структура business-модуля +### Примеры -```text -business/customer/ -├── customer.factory.ts -├── index.ts -└── types/ - ├── customer.type.ts - ├── customer-api.type.ts - ├── customer-deps.type.ts - └── customer-factory.type.ts -``` +Пример реализации фабрики в React см. в [Создание фабрики](/examples/react/factory). -### Типы - -```ts -// business/customer/types/customer-api.type.ts -export type CustomerApi = { - useCustomer: () => Customer - CustomerCard: (props: CustomerCardProps) => ReactNode -} -``` - -```ts -// business/order/types/order-deps.type.ts -export type OrderDeps = { - customer: Pick -} -``` - -```ts -// business/order/types/order-factory.type.ts -export type OrderFactory = (deps: OrderDeps) => OrderApi -``` - -### Фабрика без зависимостей - -```ts -// business/customer/customer.factory.ts -import type { CustomerFactory } from './types/customer-factory.type' - -export const customerFactory: CustomerFactory = () => { - return { - useCustomer, - CustomerCard, - } -} -``` - -### Фабрика с зависимостями - -```ts -// business/order/order.factory.ts -import type { OrderFactory } from './types/order-factory.type' - -export const orderFactory: OrderFactory = (deps) => { - return { - useOrder, - OrderCard, - } -} -``` - -### Композиция на уровне screen - -```tsx -// screens/home/home.screen.tsx -import { customerFactory } from '@/business/customer' -import { orderFactory } from '@/business/order' - -const customer = customerFactory() -const order = orderFactory({ customer }) - -const { useOrder, OrderCard } = order - -export const HomeScreen = () => { - const currentOrder = useOrder() - - return -} -``` +Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](/examples/react/factory-composition). ## Жизненный цикл diff --git a/docs/examples/react/composition-provider.md b/docs/examples/react/composition-provider.md new file mode 100644 index 0000000..d66ee4f --- /dev/null +++ b/docs/examples/react/composition-provider.md @@ -0,0 +1,249 @@ +--- +title: Композиция через Provider +description: Пример композиции бизнес-фабрик screen-модуля через React Provider +--- + +# Композиция через Provider + +Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя. + +## Идея + +Screen получает готовый API бизнес-доменов через React Context. Граф фабрик собирается снаружи, например в роутере, а внутренние `parts/` достают нужные домены через хук без пропс-дриллинга. + +## Принципы + +1. **Принадлежность.** Provider, Context и хук принадлежат конкретному screen-модулю и лежат в его сегментах. +2. **Внутренний тип.** Тип композиции не экспортируется наружу — это закрывает доступ из бизнес-модулей. +3. **Внутренний хук.** Хук доступа не экспортируется — доступен только внутри screen и его `parts/`. +4. **Публичный Provider.** Только Provider экспортируется через `index.ts`, чтобы роутер мог обернуть screen. +5. **Сборка снаружи.** Граф фабрик собирается в роутере или другом композиторе, screen фабрики не вызывает. +6. **Запрет для бизнеса.** Бизнес-модули не используют провайдеры композиции. Cross-domain зависимости передаются только через аргументы фабрики. + +## Структура модуля + +```text +screens/main/ +├── main.screen.tsx +├── providers/ +│ └── main-composition.provider.tsx +├── hooks/ +│ └── use-main-composition.hook.ts +├── types/ +│ └── main-composition.type.ts +├── parts/ +│ └── featured-products/ +│ ├── featured-products.tsx +│ └── index.ts +└── index.ts +``` + +Сегмент `providers/` — расширение стандартного набора SLM. Спецификация это разрешает: команда сама определяет, какие сегменты используются. + +## Распределение по сегментам + +| Файл | Сегмент | Назначение | +|------|---------|------------| +| `main-composition.type.ts` | `types/` | TypeScript-тип композиции | +| `main-composition.provider.tsx` | `providers/` | Context и Provider-компонент | +| `use-main-composition.hook.ts` | `hooks/` | React-хук доступа | +| `main.screen.tsx` | корень | Корневой компонент screen-модуля | +| `featured-products/` | `parts/` | Вложенный модуль со своим публичным API | + +## Тип композиции + +Файл: `screens/main/types/main-composition.type.ts`. + +Тип описывает, какие бизнес-домены доступны на этой странице. Он не экспортируется через `index.ts`, чтобы другие модули не зависели от внутренней формы композиции screen. + +```ts +import type { CatalogApi } from '@/business/catalog' +import type { CartApi } from '@/business/cart' + +export type MainComposition = { + catalog: CatalogApi + cart: CartApi +} +``` + +## Context и Provider + +Файл: `screens/main/providers/main-composition.provider.tsx`. + +Context — внутренняя деталь Provider, наружу он не экспортируется. Значение `null` по умолчанию нужно, чтобы хук мог проверить отсутствие Provider в дереве. + +Provider-компонент экспортируется через `index.ts`. Роутер передаёт в `value` уже собранный граф фабрик со стабильной ссылкой. + +```tsx +import { createContext, type ReactNode } from 'react' +import type { MainComposition } from '../types/main-composition.type' + +export const MainCompositionContext = createContext(null) + +type Props = { + value: MainComposition + children: ReactNode +} + +export const MainCompositionProvider = ({ value, children }: Props) => ( + + {children} + +) +``` + +## Хук доступа + +Файл: `screens/main/hooks/use-main-composition.hook.ts`. + +Хук остаётся внутренним и не экспортируется через `index.ts` модуля. Он доступен только внутри screen и его `parts/`. + +Если хук используется вне Provider, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева. + +```ts +import { useContext } from 'react' +import { MainCompositionContext } from '../providers/main-composition.provider' + +export const useMainComposition = () => { + const ctx = useContext(MainCompositionContext) + if (!ctx) { + throw new Error('useMainComposition must be used within MainCompositionProvider') + } + return ctx +} +``` + +## Сборка графа в роутере + +Файл: `app/router.tsx`. + +Роутер или другой композитор собирает граф фабрик в точке использования screen. Каждый домен получает свои зависимости через аргументы фабрики. + +Фабрики вызываются вне React-компонента, если не зависят от runtime-параметров. Так API доменов не пересоздаётся на каждый рендер route-компонента. + +```tsx +import { MainScreen, MainCompositionProvider } from '@/screens/main' +import { catalogFactory } from '@/business/catalog' +import { cartFactory } from '@/business/cart' +import { authFactory } from '@/business/auth' + +const auth = authFactory() +const catalog = catalogFactory() +const cart = cartFactory({ auth }) + +const MainRoute = () => ( + + + +) +``` + +## Корневой компонент screen + +Файл: `screens/main/main.screen.tsx`. + +Screen получает нужные домены из композиции и достаёт из API готовые хуки, компоненты или функции. В JSX используются уже локальные `useCategories` и `CategoryList`, а не обращение к фабричному API через точку. + +```tsx +import { useMainComposition } from './hooks/use-main-composition.hook' +import { FeaturedProducts } from './parts/featured-products' + +export const MainScreen = () => { + const { catalog } = useMainComposition() + const { useCategories, CategoryList } = catalog + const categories = useCategories() + + return ( +
+ + +
+ ) +} +``` + +## Вложенный part + +Файл: `screens/main/parts/featured-products/featured-products.tsx`. + +Вложенный модуль получает доступ к той же композиции родительского screen. Промежуточные компоненты не прокидывают домены через props. + +Из API доменов достаются готовые сущности: `useFeatured`, `ProductCard` и `addItem`. Компонент работает с ними напрямую. + +```tsx +import { useMainComposition } from '../../hooks/use-main-composition.hook' + +export const FeaturedProducts = () => { + const { catalog, cart } = useMainComposition() + const { useFeatured, ProductCard } = catalog + const { addItem } = cart + const products = useFeatured() + + return ( +
+ {products.map((product) => ( + addItem(product.id)} + /> + ))} +
+ ) +} +``` + +Файл: `screens/main/parts/featured-products/index.ts`. + +```ts +export { FeaturedProducts } from './featured-products' +``` + +## Публичный API screen-модуля + +Файл: `screens/main/index.ts`. + +Наружу экспортируются только screen и его Provider. `MainComposition`, `MainCompositionContext` и `useMainComposition` остаются деталями реализации. + +```ts +export { MainScreen } from './main.screen' +export { MainCompositionProvider } from './providers/main-composition.provider' +``` + +## Почему тип композиции не экспортируется + +Внутренний тип закрывает доступ к форме композиции из внешних модулей. Бизнес-модуль не должен знать, какие домены собраны для конкретного screen. + +Такой импорт из бизнес-модуля не должен быть возможен через публичный API screen. + +```ts +import type { MainComposition } from '@/screens/main' +``` + +Когда тип остаётся внутренним, такая связь невозможна через публичный API screen-модуля. + +## Почему хук не экспортируется + +Если хук доступа сделать публичным, любой модуль сможет вызвать его напрямую. Внутренний хук доступен только через относительные импорты внутри screen-модуля и его `parts/`. + +## Почему Provider экспортируется + +Provider безопасно экспортировать: сам по себе он не даёт доступ к данным, а только принимает готовую композицию и передаёт её детям внутри React-дерева. + +## Стабильность value + +Фабрики создаются на уровне модуля, поэтому `catalog` и `cart` сохраняют ссылки между рендерами `MainRoute`. + +Если домены зависят от runtime-параметров, граф нужно собирать в отдельном композиторе для этих параметров и передавать в Provider уже готовую композицию. + +## Расширение на другие screen-модули + +Паттерн повторяется для каждого screen, которому нужна композиция бизнес-доменов. + +```text +screens/checkout/providers/checkout-composition.provider.tsx +screens/checkout/hooks/use-checkout-composition.hook.ts +screens/checkout/types/checkout-composition.type.ts +``` + +Имена включают имя screen-модуля. Не используйте универсальные названия вроде `useComposition` или `useScope`: по имени файла должно быть понятно, к какой странице привязан Context. diff --git a/docs/examples/react/factory-composition.md b/docs/examples/react/factory-composition.md new file mode 100644 index 0000000..d1a5997 --- /dev/null +++ b/docs/examples/react/factory-composition.md @@ -0,0 +1,52 @@ +--- +title: Композиция фабрик +description: Пример композиции business-фабрик на уровне screen-модуля в React-проекте +--- + +# Композиция фабрик + +Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов. + +## Идея + +Композиция фабрик выполняется в модуле-потребителе: screen, layout или другом модуле группы «Композиция». Business-модули не импортируют runtime-код друг друга напрямую, а cross-domain зависимости получают только через аргументы фабрик. + +## Структура screen-модуля + +```text +screens/home/ +├── home.screen.tsx +└── index.ts +``` + +## Сборка фабрик + +Файл: `screens/home/home.screen.tsx`. + +```tsx +import { customerFactory } from '@/business/customer' +import { orderFactory } from '@/business/order' + +const customer = customerFactory() +const order = orderFactory({ customer }) + +const { useOrder, OrderCard } = order + +export const HomeScreen = () => { + const currentOrder = useOrder() + + return +} +``` + +`customerFactory` создаётся первой, потому что `orderFactory` зависит от части API домена `customer`. Модуль `order` не импортирует `customer` в runtime — зависимость передаётся снаружи. + +## Публичный API screen-модуля + +Файл: `screens/home/index.ts`. + +```ts +export { HomeScreen } from './home.screen' +``` + +Screen экспортирует только собственный публичный API. Собранные экземпляры business API остаются деталями реализации screen-модуля. diff --git a/docs/examples/react/factory.md b/docs/examples/react/factory.md new file mode 100644 index 0000000..94622a6 --- /dev/null +++ b/docs/examples/react/factory.md @@ -0,0 +1,114 @@ +--- +title: Создание фабрики +description: Пример создания фабрики business-модуля в React-проекте +--- + +# Создание фабрики + +Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API. + +## Структура business-модуля + +Фабрика лежит в корне business-модуля. Типы публичного API и зависимостей размещаются в `types/`. + +```text +business/customer/ +├── customer.factory.ts +├── hooks/ +├── types/ +│ ├── customer.type.ts +│ ├── customer-api.type.ts +│ └── customer-factory.type.ts +├── ui/ +└── index.ts +``` + +## Тип публичного API + +Публичный API описывает runtime-возможности, которые модуль отдаёт потребителям: хуки, компоненты и сценарные методы. + +```ts +// business/customer/types/customer-api.type.ts +import type { ReactNode } from 'react' +import type { Customer } from './customer.type' + +export type CustomerCardProps = { + customer: Customer +} + +export type CustomerApi = { + useCustomer: () => Customer | null + CustomerCard: (props: CustomerCardProps) => ReactNode +} +``` + +```ts +// business/customer/types/customer-factory.type.ts +import type { CustomerApi } from './customer-api.type' + +export type CustomerFactory = () => CustomerApi +``` + +## Фабрика без зависимостей + +Если модулю не нужны другие домены в runtime, фабрика создаётся без аргументов. + +```ts +// business/customer/customer.factory.ts +import { useCustomer } from './hooks/use-customer.hook' +import { CustomerCard } from './ui/customer-card' +import type { CustomerFactory } from './types/customer-factory.type' + +export const customerFactory: CustomerFactory = () => { + return { + useCustomer, + CustomerCard, + } +} +``` + +```ts +// business/customer/index.ts +export { customerFactory } from './customer.factory' + +export type { Customer } from './types/customer.type' +export type { CustomerApi } from './types/customer-api.type' +export type { CustomerFactory } from './types/customer-factory.type' +``` + +## Фабрика с зависимостями + +Если модулю нужен другой домен в runtime, зависимость передаётся аргументом фабрики. Тип зависимости описывает только нужную часть API. + +```ts +// business/order/types/order-deps.type.ts +import type { CustomerApi } from '@/business/customer' + +export type OrderDeps = { + customer: Pick +} +``` + +```ts +// business/order/types/order-factory.type.ts +import type { OrderApi } from './order-api.type' +import type { OrderDeps } from './order-deps.type' + +export type OrderFactory = (deps: OrderDeps) => OrderApi +``` + +```ts +// business/order/order.factory.ts +import { createUseOrder } from './hooks/use-order.hook' +import { OrderCard } from './ui/order-card' +import type { OrderFactory } from './types/order-factory.type' + +export const orderFactory: OrderFactory = (deps) => { + const useOrder = createUseOrder(deps) + + return { + useOrder, + OrderCard, + } +} +``` diff --git a/public/ARCHITECTURE.md b/public/ARCHITECTURE.md index 51c0a61..87c49a3 100644 --- a/public/ARCHITECTURE.md +++ b/public/ARCHITECTURE.md @@ -556,87 +556,11 @@ Business-модуль всегда экспортирует фабрику. Фа Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция». -#### Структура business-модуля +#### Примеры -```text -business/customer/ -├── customer.factory.ts -├── index.ts -└── types/ - ├── customer.type.ts - ├── customer-api.type.ts - ├── customer-deps.type.ts - └── customer-factory.type.ts -``` +Пример реализации фабрики в React см. в [Создание фабрики](/examples/react/factory). -#### Типы - -```ts -// business/customer/types/customer-api.type.ts -export type CustomerApi = { - useCustomer: () => Customer - CustomerCard: (props: CustomerCardProps) => ReactNode -} -``` - -```ts -// business/order/types/order-deps.type.ts -export type OrderDeps = { - customer: Pick -} -``` - -```ts -// business/order/types/order-factory.type.ts -export type OrderFactory = (deps: OrderDeps) => OrderApi -``` - -#### Фабрика без зависимостей - -```ts -// business/customer/customer.factory.ts -import type { CustomerFactory } from './types/customer-factory.type' - -export const customerFactory: CustomerFactory = () => { - return { - useCustomer, - CustomerCard, - } -} -``` - -#### Фабрика с зависимостями - -```ts -// business/order/order.factory.ts -import type { OrderFactory } from './types/order-factory.type' - -export const orderFactory: OrderFactory = (deps) => { - return { - useOrder, - OrderCard, - } -} -``` - -#### Композиция на уровне screen - -```tsx -// screens/home/home.screen.tsx -import { customerFactory } from '@/business/customer' -import { orderFactory } from '@/business/order' - -const customer = customerFactory() -const order = orderFactory({ customer }) - -const { useOrder, OrderCard } = order - -export const HomeScreen = () => { - const currentOrder = useOrder() - - return -} -``` +Пример композиции фабрик в React screen-модуле см. в [Композиция фабрик](/examples/react/factory-composition). ### Жизненный цикл @@ -1058,4 +982,410 @@ packages/ui/promo-section/ - Пакеты не импортируют приложения. - Межпакетные импорты идут только через публичный API. - Deep imports внутрь пакетов запрещены. -- Локальная колокация важнее преждевременного выноса в `packages/*`. \ No newline at end of file +- Локальная колокация важнее преждевременного выноса в `packages/*`. + + +## Создание фабрики + +Раздел показывает, как оформить фабрику business-модуля в React-проекте: описать публичный API, зависимости и функцию, возвращающую runtime API. + +### Структура business-модуля + +Фабрика лежит в корне business-модуля. Типы публичного API и зависимостей размещаются в `types/`. + +```text +business/customer/ +├── customer.factory.ts +├── hooks/ +├── types/ +│ ├── customer.type.ts +│ ├── customer-api.type.ts +│ └── customer-factory.type.ts +├── ui/ +└── index.ts +``` + +### Тип публичного API + +Публичный API описывает runtime-возможности, которые модуль отдаёт потребителям: хуки, компоненты и сценарные методы. + +```ts +// business/customer/types/customer-api.type.ts +import type { ReactNode } from 'react' +import type { Customer } from './customer.type' + +export type CustomerCardProps = { + customer: Customer +} + +export type CustomerApi = { + useCustomer: () => Customer | null + CustomerCard: (props: CustomerCardProps) => ReactNode +} +``` + +```ts +// business/customer/types/customer-factory.type.ts +import type { CustomerApi } from './customer-api.type' + +export type CustomerFactory = () => CustomerApi +``` + +### Фабрика без зависимостей + +Если модулю не нужны другие домены в runtime, фабрика создаётся без аргументов. + +```ts +// business/customer/customer.factory.ts +import { useCustomer } from './hooks/use-customer.hook' +import { CustomerCard } from './ui/customer-card' +import type { CustomerFactory } from './types/customer-factory.type' + +export const customerFactory: CustomerFactory = () => { + return { + useCustomer, + CustomerCard, + } +} +``` + +```ts +// business/customer/index.ts +export { customerFactory } from './customer.factory' + +export type { Customer } from './types/customer.type' +export type { CustomerApi } from './types/customer-api.type' +export type { CustomerFactory } from './types/customer-factory.type' +``` + +### Фабрика с зависимостями + +Если модулю нужен другой домен в runtime, зависимость передаётся аргументом фабрики. Тип зависимости описывает только нужную часть API. + +```ts +// business/order/types/order-deps.type.ts +import type { CustomerApi } from '@/business/customer' + +export type OrderDeps = { + customer: Pick +} +``` + +```ts +// business/order/types/order-factory.type.ts +import type { OrderApi } from './order-api.type' +import type { OrderDeps } from './order-deps.type' + +export type OrderFactory = (deps: OrderDeps) => OrderApi +``` + +```ts +// business/order/order.factory.ts +import { createUseOrder } from './hooks/use-order.hook' +import { OrderCard } from './ui/order-card' +import type { OrderFactory } from './types/order-factory.type' + +export const orderFactory: OrderFactory = (deps) => { + const useOrder = createUseOrder(deps) + + return { + useOrder, + OrderCard, + } +} +``` + + +## Композиция фабрик + +Раздел показывает, как собрать API нескольких business-модулей в React screen-модуле. Пример подходит для простой композиции, когда screen сам является точкой использования доменов. + +### Идея + +Композиция фабрик выполняется в модуле-потребителе: screen, layout или другом модуле группы «Композиция». Business-модули не импортируют runtime-код друг друга напрямую, а cross-domain зависимости получают только через аргументы фабрик. + +### Структура screen-модуля + +```text +screens/home/ +├── home.screen.tsx +└── index.ts +``` + +### Сборка фабрик + +Файл: `screens/home/home.screen.tsx`. + +```tsx +import { customerFactory } from '@/business/customer' +import { orderFactory } from '@/business/order' + +const customer = customerFactory() +const order = orderFactory({ customer }) + +const { useOrder, OrderCard } = order + +export const HomeScreen = () => { + const currentOrder = useOrder() + + return +} +``` + +`customerFactory` создаётся первой, потому что `orderFactory` зависит от части API домена `customer`. Модуль `order` не импортирует `customer` в runtime — зависимость передаётся снаружи. + +### Публичный API screen-модуля + +Файл: `screens/home/index.ts`. + +```ts +export { HomeScreen } from './home.screen' +``` + +Screen экспортирует только собственный публичный API. Собранные экземпляры business API остаются деталями реализации screen-модуля. + + +## Композиция через Provider + +Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя. + +### Идея + +Screen получает готовый API бизнес-доменов через React Context. Граф фабрик собирается снаружи, например в роутере, а внутренние `parts/` достают нужные домены через хук без пропс-дриллинга. + +### Принципы + +1. **Принадлежность.** Provider, Context и хук принадлежат конкретному screen-модулю и лежат в его сегментах. +2. **Внутренний тип.** Тип композиции не экспортируется наружу — это закрывает доступ из бизнес-модулей. +3. **Внутренний хук.** Хук доступа не экспортируется — доступен только внутри screen и его `parts/`. +4. **Публичный Provider.** Только Provider экспортируется через `index.ts`, чтобы роутер мог обернуть screen. +5. **Сборка снаружи.** Граф фабрик собирается в роутере или другом композиторе, screen фабрики не вызывает. +6. **Запрет для бизнеса.** Бизнес-модули не используют провайдеры композиции. Cross-domain зависимости передаются только через аргументы фабрики. + +### Структура модуля + +```text +screens/main/ +├── main.screen.tsx +├── providers/ +│ └── main-composition.provider.tsx +├── hooks/ +│ └── use-main-composition.hook.ts +├── types/ +│ └── main-composition.type.ts +├── parts/ +│ └── featured-products/ +│ ├── featured-products.tsx +│ └── index.ts +└── index.ts +``` + +Сегмент `providers/` — расширение стандартного набора SLM. Спецификация это разрешает: команда сама определяет, какие сегменты используются. + +### Распределение по сегментам + +| Файл | Сегмент | Назначение | +|------|---------|------------| +| `main-composition.type.ts` | `types/` | TypeScript-тип композиции | +| `main-composition.provider.tsx` | `providers/` | Context и Provider-компонент | +| `use-main-composition.hook.ts` | `hooks/` | React-хук доступа | +| `main.screen.tsx` | корень | Корневой компонент screen-модуля | +| `featured-products/` | `parts/` | Вложенный модуль со своим публичным API | + +### Тип композиции + +Файл: `screens/main/types/main-composition.type.ts`. + +Тип описывает, какие бизнес-домены доступны на этой странице. Он не экспортируется через `index.ts`, чтобы другие модули не зависели от внутренней формы композиции screen. + +```ts +import type { CatalogApi } from '@/business/catalog' +import type { CartApi } from '@/business/cart' + +export type MainComposition = { + catalog: CatalogApi + cart: CartApi +} +``` + +### Context и Provider + +Файл: `screens/main/providers/main-composition.provider.tsx`. + +Context — внутренняя деталь Provider, наружу он не экспортируется. Значение `null` по умолчанию нужно, чтобы хук мог проверить отсутствие Provider в дереве. + +Provider-компонент экспортируется через `index.ts`. Роутер передаёт в `value` уже собранный граф фабрик со стабильной ссылкой. + +```tsx +import { createContext, type ReactNode } from 'react' +import type { MainComposition } from '../types/main-composition.type' + +export const MainCompositionContext = createContext(null) + +type Props = { + value: MainComposition + children: ReactNode +} + +export const MainCompositionProvider = ({ value, children }: Props) => ( + + {children} + +) +``` + +### Хук доступа + +Файл: `screens/main/hooks/use-main-composition.hook.ts`. + +Хук остаётся внутренним и не экспортируется через `index.ts` модуля. Он доступен только внутри screen и его `parts/`. + +Если хук используется вне Provider, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева. + +```ts +import { useContext } from 'react' +import { MainCompositionContext } from '../providers/main-composition.provider' + +export const useMainComposition = () => { + const ctx = useContext(MainCompositionContext) + if (!ctx) { + throw new Error('useMainComposition must be used within MainCompositionProvider') + } + return ctx +} +``` + +### Сборка графа в роутере + +Файл: `app/router.tsx`. + +Роутер или другой композитор собирает граф фабрик в точке использования screen. Каждый домен получает свои зависимости через аргументы фабрики. + +Фабрики вызываются вне React-компонента, если не зависят от runtime-параметров. Так API доменов не пересоздаётся на каждый рендер route-компонента. + +```tsx +import { MainScreen, MainCompositionProvider } from '@/screens/main' +import { catalogFactory } from '@/business/catalog' +import { cartFactory } from '@/business/cart' +import { authFactory } from '@/business/auth' + +const auth = authFactory() +const catalog = catalogFactory() +const cart = cartFactory({ auth }) + +const MainRoute = () => ( + + + +) +``` + +### Корневой компонент screen + +Файл: `screens/main/main.screen.tsx`. + +Screen получает нужные домены из композиции и достаёт из API готовые хуки, компоненты или функции. В JSX используются уже локальные `useCategories` и `CategoryList`, а не обращение к фабричному API через точку. + +```tsx +import { useMainComposition } from './hooks/use-main-composition.hook' +import { FeaturedProducts } from './parts/featured-products' + +export const MainScreen = () => { + const { catalog } = useMainComposition() + const { useCategories, CategoryList } = catalog + const categories = useCategories() + + return ( +
+ + +
+ ) +} +``` + +### Вложенный part + +Файл: `screens/main/parts/featured-products/featured-products.tsx`. + +Вложенный модуль получает доступ к той же композиции родительского screen. Промежуточные компоненты не прокидывают домены через props. + +Из API доменов достаются готовые сущности: `useFeatured`, `ProductCard` и `addItem`. Компонент работает с ними напрямую. + +```tsx +import { useMainComposition } from '../../hooks/use-main-composition.hook' + +export const FeaturedProducts = () => { + const { catalog, cart } = useMainComposition() + const { useFeatured, ProductCard } = catalog + const { addItem } = cart + const products = useFeatured() + + return ( +
+ {products.map((product) => ( + addItem(product.id)} + /> + ))} +
+ ) +} +``` + +Файл: `screens/main/parts/featured-products/index.ts`. + +```ts +export { FeaturedProducts } from './featured-products' +``` + +### Публичный API screen-модуля + +Файл: `screens/main/index.ts`. + +Наружу экспортируются только screen и его Provider. `MainComposition`, `MainCompositionContext` и `useMainComposition` остаются деталями реализации. + +```ts +export { MainScreen } from './main.screen' +export { MainCompositionProvider } from './providers/main-composition.provider' +``` + +### Почему тип композиции не экспортируется + +Внутренний тип закрывает доступ к форме композиции из внешних модулей. Бизнес-модуль не должен знать, какие домены собраны для конкретного screen. + +Такой импорт из бизнес-модуля не должен быть возможен через публичный API screen. + +```ts +import type { MainComposition } from '@/screens/main' +``` + +Когда тип остаётся внутренним, такая связь невозможна через публичный API screen-модуля. + +### Почему хук не экспортируется + +Если хук доступа сделать публичным, любой модуль сможет вызвать его напрямую. Внутренний хук доступен только через относительные импорты внутри screen-модуля и его `parts/`. + +### Почему Provider экспортируется + +Provider безопасно экспортировать: сам по себе он не даёт доступ к данным, а только принимает готовую композицию и передаёт её детям внутри React-дерева. + +### Стабильность value + +Фабрики создаются на уровне модуля, поэтому `catalog` и `cart` сохраняют ссылки между рендерами `MainRoute`. + +Если домены зависят от runtime-параметров, граф нужно собирать в отдельном композиторе для этих параметров и передавать в Provider уже готовую композицию. + +### Расширение на другие screen-модули + +Паттерн повторяется для каждого screen, которому нужна композиция бизнес-доменов. + +```text +screens/checkout/providers/checkout-composition.provider.tsx +screens/checkout/hooks/use-checkout-composition.hook.ts +screens/checkout/types/checkout-composition.type.ts +``` + +Имена включают имя screen-модуля. Не используйте универсальные названия вроде `useComposition` или `useScope`: по имени файла должно быть понятно, к какой странице привязан Context. \ No newline at end of file