--- 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((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 type ProfilePageProviderValue = { store: StoreApi business: ProfileBusinessComposition } export const ProfilePageContext = createContext(null) type Props = { children: ReactNode } export const ProfilePageProvider = ({ children }: Props) => { const valueRef = useRef(null) if (!valueRef.current) { valueRef.current = { store: createProfilePageStore(), business: createProfileBusinessComposition(), } } return ( {children} ) } ``` 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 = (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 (
{title}
{children}
) } ``` 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 } ``` 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: ( ), } ``` В 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 ( {children} ) } ``` ```tsx // app/(profile)/page.tsx import { ProfileScreen } from '@/compositions/screens/profile' export default function Page() { return } ``` `app` размещает готовые composition modules по правилам фреймворка, но не реализует их внутри себя.