Files
docs/projects/slm-design/canons/examples/react/composition-provider.md
S.Gromov 89cc873c19
All checks were successful
CI/CD Pipeline / build (push) Successful in 44s
CI/CD Pipeline / docker (push) Successful in 1m17s
CI/CD Pipeline / deploy (push) Successful in 8s
docs: обновить архитектуру SLM compositions
- обновлена модель слоёв на app → compositions → business → infra → ui → shared
- добавлены правила composition modules и providers-сегмента
- обновлены правила монорепозитория для слоя compositions
- переписаны React-примеры под page-level композицию
- добавлен пример вариантов структуры compositions
2026-05-26 23:46:11 +03:00

287 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Композиция через Provider
description: Пример page-level Provider для composition modules в React-проекте
---
# Композиция через Provider
Раздел показывает, как page composition может владеть provider, store и business composition, которые нужны layout, screen и другим composition modules.
## Идея
Page composition хранит состояние и композицию бизнес-доменов на уровне страницы. Layout и screen не импортируют друг друга: они получают доступ к page-level данным через публичный API page composition.
В примере page composition владеет scope-контрактом страницы, но не экспортирует готовый `ProfilePage`, потому что layout и screen импортируют hooks из `pages/profile`. Дерево страницы собирается в `app` или в отдельном entry-point composition module.
## Принципы
1. **Владение.** Page-level store, provider и business composition принадлежат page composition module.
2. **Обычные сегменты.** Provider, hooks, stores и types лежат в обычных сегментах модуля: `providers/`, `hooks/`, `stores/`, `types/`.
3. **Публичный контракт.** Page composition экспортирует только безопасные hooks, provider и типы, которые нужны другим composition modules или `app`.
4. **Сборка снаружи business.** Business-модули не используют page-level providers. Cross-domain зависимости передаются только через аргументы фабрики.
5. **Без deep imports.** Layout и screen импортируют hooks только из public API page composition.
## Структура модулей
```text
compositions/pages/profile/
├── profile-business-composition.ts
├── providers/
│ └── profile-page.provider.tsx
├── hooks/
│ ├── use-profile-page-store.hook.ts
│ └── use-profile-business-composition.hook.ts
├── stores/
│ └── profile-page.store.ts
├── types/
│ └── profile-page-state.type.ts
└── index.ts
compositions/layouts/profile-main/
├── profile-main.layout.tsx
└── index.ts
compositions/screens/profile/
├── profile.screen.tsx
└── index.ts
```
## Тип состояния страницы
Файл: `compositions/pages/profile/types/profile-page-state.type.ts`.
```ts
export type ProfilePageState = {
title: string
isSidebarOpen: boolean
setSidebarOpen: (value: boolean) => void
}
```
## Store страницы
Файл: `compositions/pages/profile/stores/profile-page.store.ts`.
```ts
import { createStore } from 'zustand/vanilla'
import type { ProfilePageState } from '../types/profile-page-state.type'
export const createProfilePageStore = () =>
createStore<ProfilePageState>((set) => ({
title: 'Profile',
isSidebarOpen: false,
setSidebarOpen: (value) => set({ isSidebarOpen: value }),
}))
```
`createProfilePageStore` не экспортируется через public API модуля. Это внутренняя деталь создания состояния.
## Business composition страницы
Файл: `compositions/pages/profile/profile-business-composition.ts`.
```ts
import { authFactory } from '@/business/auth'
import { profileFactory } from '@/business/profile'
export const createProfileBusinessComposition = () => {
const auth = authFactory()
const profile = profileFactory({ auth })
return { auth, profile }
}
```
Business composition собирается на слое `compositions`, а не внутри business-модулей.
## Provider страницы
Файл: `compositions/pages/profile/providers/profile-page.provider.tsx`.
```tsx
import { createContext, useRef, type ReactNode } from 'react'
import type { StoreApi } from 'zustand/vanilla'
import { createProfileBusinessComposition } from '../profile-business-composition'
import { createProfilePageStore } from '../stores/profile-page.store'
import type { ProfilePageState } from '../types/profile-page-state.type'
type ProfileBusinessComposition = ReturnType<typeof createProfileBusinessComposition>
type ProfilePageProviderValue = {
store: StoreApi<ProfilePageState>
business: ProfileBusinessComposition
}
export const ProfilePageContext = createContext<ProfilePageProviderValue | null>(null)
type Props = {
children: ReactNode
}
export const ProfilePageProvider = ({ children }: Props) => {
const valueRef = useRef<ProfilePageProviderValue | null>(null)
if (!valueRef.current) {
valueRef.current = {
store: createProfilePageStore(),
business: createProfileBusinessComposition(),
}
}
return (
<ProfilePageContext.Provider value={valueRef.current}>
{children}
</ProfilePageContext.Provider>
)
}
```
Context object остаётся технической деталью provider и не должен использоваться внешними модулями напрямую. Наружу экспортируются hooks доступа.
## Hooks доступа
Файл: `compositions/pages/profile/hooks/use-profile-page-store.hook.ts`.
```ts
import { useContext } from 'react'
import { useStore } from 'zustand'
import { ProfilePageContext } from '../providers/profile-page.provider'
import type { ProfilePageState } from '../types/profile-page-state.type'
export const useProfilePageStore = <T,>(selector: (state: ProfilePageState) => T) => {
const ctx = useContext(ProfilePageContext)
if (!ctx) {
throw new Error('useProfilePageStore must be used within ProfilePageProvider')
}
return useStore(ctx.store, selector)
}
```
Файл: `compositions/pages/profile/hooks/use-profile-business-composition.hook.ts`.
```ts
import { useContext } from 'react'
import { ProfilePageContext } from '../providers/profile-page.provider'
export const useProfileBusinessComposition = () => {
const ctx = useContext(ProfilePageContext)
if (!ctx) {
throw new Error('useProfileBusinessComposition must be used within ProfilePageProvider')
}
return ctx.business
}
```
## Layout использует page-level store
Файл: `compositions/layouts/profile-main/profile-main.layout.tsx`.
```tsx
import type { ReactNode } from 'react'
import { useProfilePageStore } from '@/compositions/pages/profile'
type Props = {
children: ReactNode
}
export const ProfileMainLayout = ({ children }: Props) => {
const title = useProfilePageStore((state) => state.title)
const isSidebarOpen = useProfilePageStore((state) => state.isSidebarOpen)
return (
<div data-sidebar-open={isSidebarOpen}>
<header>{title}</header>
<main>{children}</main>
</div>
)
}
```
Layout импортирует hook из public API page composition. Он не импортирует screen и не лезет во внутренние файлы `pages/profile`.
## Screen использует business composition
Файл: `compositions/screens/profile/profile.screen.tsx`.
```tsx
import { useProfileBusinessComposition } from '@/compositions/pages/profile'
export const ProfileScreen = () => {
const { profile } = useProfileBusinessComposition()
const { useCurrentProfile, ProfileCard } = profile
const currentProfile = useCurrentProfile()
return <ProfileCard profile={currentProfile} />
}
```
Screen получает готовые доменные API из page composition и не собирает граф фабрик самостоятельно.
## Публичный API page composition
Файл: `compositions/pages/profile/index.ts`.
```ts
export { ProfilePageProvider } from './providers/profile-page.provider'
export { useProfilePageStore } from './hooks/use-profile-page-store.hook'
export { useProfileBusinessComposition } from './hooks/use-profile-business-composition.hook'
export type { ProfilePageState } from './types/profile-page-state.type'
```
Внутренние `createProfilePageStore`, `createProfileBusinessComposition` и `ProfilePageContext` не экспортируются через public API.
Если нужен готовый `ProfilePage`, его лучше собрать в отдельном entry-point composition module или прямо в роутере. Не смешивайте в одном public API и готовую page composition, и hooks, которые импортируют её дочерние layout/screen modules: это может создать runtime-цикл.
## Подключение в app
В React Router можно собрать дерево прямо в route config:
```tsx
import { ProfilePageProvider } from '@/compositions/pages/profile'
import { ProfileMainLayout } from '@/compositions/layouts/profile-main'
import { ProfileScreen } from '@/compositions/screens/profile'
export const profileRoute = {
path: '/profile',
element: (
<ProfilePageProvider>
<ProfileMainLayout>
<ProfileScreen />
</ProfileMainLayout>
</ProfilePageProvider>
),
}
```
В Next App Router композиция может быть физически разложена по файлам `app`, но реализация остаётся в `compositions`.
```tsx
// app/(profile)/layout.tsx
import { ProfilePageProvider } from '@/compositions/pages/profile'
import { ProfileMainLayout } from '@/compositions/layouts/profile-main'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<ProfilePageProvider>
<ProfileMainLayout>{children}</ProfileMainLayout>
</ProfilePageProvider>
)
}
```
```tsx
// app/(profile)/page.tsx
import { ProfileScreen } from '@/compositions/screens/profile'
export default function Page() {
return <ProfileScreen />
}
```
`app` размещает готовые composition modules по правилам фреймворка, но не реализует их внутри себя.