feat: добавить хаб документаций
- добавлен React/Vite-лендинг с карточками документаций - добавлена генерация корневого llms.txt из конфига документов - добавлена сборка SLM Design через VitePress - добавлены Dockerfile, Caddyfile и Gitea CI/CD - настроены контекстные Link headers для llms.txt
This commit is contained in:
249
canons/slm-design/examples/react/composition-provider.md
Normal file
249
canons/slm-design/examples/react/composition-provider.md
Normal file
@@ -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<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, он бросает ошибку. Это даёт раннюю диагностику неправильной композиции дерева.
|
||||
|
||||
```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 = () => (
|
||||
<MainCompositionProvider value={{ catalog, cart }}>
|
||||
<MainScreen />
|
||||
</MainCompositionProvider>
|
||||
)
|
||||
```
|
||||
|
||||
## Корневой компонент 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 (
|
||||
<div>
|
||||
<CategoryList categories={categories} />
|
||||
<FeaturedProducts />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Вложенный 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 (
|
||||
<div>
|
||||
{products.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
onAdd={() => addItem(product.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Файл: `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.
|
||||
Reference in New Issue
Block a user