--- title: Композиция через Provider description: "Раздел показывает, как screen-модуль может получить готовую композицию бизнес-доменов через React Context, не вызывая фабрики внутри себя." --- # Композиция через 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.