Files
docs/canons/slm-design/examples/react/composition-provider.md
S.Gromov 86ab6bc8fd feat: добавить хаб документаций
- добавлен React/Vite-лендинг с карточками документаций
- добавлена генерация корневого llms.txt из конфига документов
- добавлена сборка SLM Design через VitePress
- добавлены Dockerfile, Caddyfile и Gitea CI/CD
- настроены контекстные Link headers для llms.txt
2026-05-13 10:12:31 +03:00

11 KiB
Raw Blame History

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

Структура модуля

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.

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 уже собранный граф фабрик со стабильной ссылкой.

import { createContext, type ReactNode } from 'react'
import type { MainComposition } from '../types/main-composition.type'

export const MainCompositionContext = createContext<MainComposition | null>(null)

type Props = {
  value: MainComposition
  children: ReactNode
}

export const MainCompositionProvider = ({ value, children }: Props) => (
  <MainCompositionContext.Provider value={value}>
    {children}
  </MainCompositionContext.Provider>
)

Хук доступа

Файл: screens/main/hooks/use-main-composition.hook.ts.

Хук остаётся внутренним и не экспортируется через index.ts модуля. Он доступен только внутри screen и его parts/.

Если хук используется вне Provider, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева.

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-компонента.

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 = () => (
  <MainCompositionProvider value={{ catalog, cart }}>
    <MainScreen />
  </MainCompositionProvider>
)

Корневой компонент screen

Файл: screens/main/main.screen.tsx.

Screen получает нужные домены из композиции и достаёт из API готовые хуки, компоненты или функции. В JSX используются уже локальные useCategories и CategoryList, а не обращение к фабричному API через точку.

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 (
    <div>
      <CategoryList categories={categories} />
      <FeaturedProducts />
    </div>
  )
}

Вложенный part

Файл: screens/main/parts/featured-products/featured-products.tsx.

Вложенный модуль получает доступ к той же композиции родительского screen. Промежуточные компоненты не прокидывают домены через props.

Из API доменов достаются готовые сущности: useFeatured, ProductCard и addItem. Компонент работает с ними напрямую.

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 (
    <div>
      {products.map((product) => (
        <ProductCard
          key={product.id}
          product={product}
          onAdd={() => addItem(product.id)}
        />
      ))}
    </div>
  )
}

Файл: screens/main/parts/featured-products/index.ts.

export { FeaturedProducts } from './featured-products'

Публичный API screen-модуля

Файл: screens/main/index.ts.

Наружу экспортируются только screen и его Provider. MainComposition, MainCompositionContext и useMainComposition остаются деталями реализации.

export { MainScreen } from './main.screen'
export { MainCompositionProvider } from './providers/main-composition.provider'

Почему тип композиции не экспортируется

Внутренний тип закрывает доступ к форме композиции из внешних модулей. Бизнес-модуль не должен знать, какие домены собраны для конкретного screen.

Такой импорт из бизнес-модуля не должен быть возможен через публичный API screen.

import type { MainComposition } from '@/screens/main'

Когда тип остаётся внутренним, такая связь невозможна через публичный API screen-модуля.

Почему хук не экспортируется

Если хук доступа сделать публичным, любой модуль сможет вызвать его напрямую. Внутренний хук доступен только через относительные импорты внутри screen-модуля и его parts/.

Почему Provider экспортируется

Provider безопасно экспортировать: сам по себе он не даёт доступ к данным, а только принимает готовую композицию и передаёт её детям внутри React-дерева.

Стабильность value

Фабрики создаются на уровне модуля, поэтому catalog и cart сохраняют ссылки между рендерами MainRoute.

Если домены зависят от runtime-параметров, граф нужно собирать в отдельном композиторе для этих параметров и передавать в Provider уже готовую композицию.

Расширение на другие screen-модули

Паттерн повторяется для каждого screen, которому нужна композиция бизнес-доменов.

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.