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