Frameworks frontend
Les exemples frontend créent des scopes pour les pages, routes, écrans ou fonctionnalités. Ils utilisent leurs propres petits constructeurs de graphe plutôt que le module serveur partagé.
Compare la création du scope, sa transmission aux enfants et sa libération au démontage.
| Exemple | Contenu |
|---|---|
react.tsx | Scope de fonctionnalité React géré par @inferdi/react |
react-native.tsx | Scope d’écran React Native |
vue.ts | Limite de scope avec provide/inject de Vue 3 |
svelte.ts | Limite de scope via le contexte Svelte |
React
tsx
import { Container } from '@inferdi/inferdi'
import {inferdiReact} from '@inferdi/react'
import {type PropsWithChildren} from 'react'
/*
* Frontend examples keep their own minimal builder because the browser does
* not have the `Database` / `process.env` shape that `_shared/container.ts`
* uses on the server. The patterns are the same: scoped `FeatureContext`,
* singleton services on the root, page-level scope after commit
*/
type FeatureContext = {
readonly featureName: string
}
class ApiClient {
async listProjects() {
return [{ id: 'project_1', name: 'Docs' }]
}
}
class ProjectsViewModel {
constructor(
private readonly feature: FeatureContext,
private readonly api: ApiClient
) {}
async load() {
return {
feature: this.feature.featureName,
projects: await this.api.listProjects()
}
}
}
const root = new Container()
.declareScopeInputs<{ feature: FeatureContext }>()
.registerClass('api', ApiClient, [])
.registerClass('projectsVm', ProjectsViewModel, ['feature', 'api'], 'scoped')
type AppContainer = typeof root
type PageContainer = ReturnType<typeof createProjectsPageScope>
function createProjectsPageScope(parent: AppContainer) {
return parent.createScope({
feature: { featureName: 'projects' }
})
}
const AppDI = inferdiReact<AppContainer>()
const PageDI = AppDI.createScope({
createScope: (parent): PageContainer => createProjectsPageScope(parent),
onDisposeError: (error) => console.error(error)
})
export function AppDIProvider({ children }: PropsWithChildren) {
return <AppDI.Provider container={root}>{children}</AppDI.Provider>
}
export function ProjectsPage({ children }: PropsWithChildren) {
return <PageDI.ScopeProvider>{children}</PageDI.ScopeProvider>
}
export function useProjectsViewModel() {
return PageDI.useService('projectsVm')
}Fichier du dépôt : examples/frontend/react.tsx
React Native
tsx
import { Container } from '@inferdi/inferdi'
import {
createContext,
type PropsWithChildren,
useContext,
useEffect,
useState
} from 'react'
type ScreenContext = {
readonly screenName: string
}
class DeviceStorage {
async getItem(key: string) {
return `value:${key}`
}
}
class SettingsViewModel {
constructor(
private readonly screen: ScreenContext,
private readonly storage: DeviceStorage
) {}
async loadTheme() {
return {
screen: this.screen.screenName,
theme: await this.storage.getItem('theme')
}
}
}
const root = new Container()
.declareScopeInputs<{ screen: ScreenContext }>()
.registerClass('storage', DeviceStorage, [])
.registerClass('settingsVm', SettingsViewModel, ['screen', 'storage'], 'scoped')
type AppContainer = typeof root
type ScreenContainer = ReturnType<typeof createSettingsScreenScope>
function createSettingsScreenScope(parent: AppContainer) {
return parent.createScope({
screen: { screenName: 'settings' }
})
}
const RootDIContext = createContext<AppContainer | null>(null)
const ScreenDIContext = createContext<ScreenContainer | null>(null)
export function AppDIProvider({ children }: PropsWithChildren) {
return <RootDIContext.Provider value={root}>{children}</RootDIContext.Provider>
}
export function SettingsScreenScope({ children }: PropsWithChildren) {
const parent = useContext(RootDIContext)
if (parent === null) throw new Error('DI provider is missing')
const [scope, setScope] = useState<ScreenContainer | null>(null)
useEffect(() => {
const nextScope = createSettingsScreenScope(parent)
setScope(nextScope)
return () => {
nextScope.dispose().catch(console.error)
}
}, [parent])
if (scope === null) return null
return <ScreenDIContext.Provider value={scope}>{children}</ScreenDIContext.Provider>
}
export function useSettingsViewModel() {
const container = useContext(ScreenDIContext)
if (container === null) throw new Error('Settings screen scope is missing')
return container.get('settingsVm')
}Fichier du dépôt : examples/frontend/react-native.tsx
Vue
ts
import { Container } from '@inferdi/inferdi'
import {
defineComponent,
inject,
onUnmounted,
provide,
type InjectionKey
} from 'vue'
type RouteContext = {
readonly routeName: string
}
class ApiClient {
async dashboard() {
return { total: 42 }
}
}
class DashboardViewModel {
constructor(
private readonly route: RouteContext,
private readonly api: ApiClient
) {}
async load() {
return {
route: this.route.routeName,
dashboard: await this.api.dashboard()
}
}
}
const root = new Container()
.declareScopeInputs<{ route: RouteContext }>()
.registerClass('api', ApiClient, [])
.registerClass('dashboardVm', DashboardViewModel, ['route', 'api'], 'scoped')
type AppContainer = typeof root
type RouteContainer = ReturnType<typeof createDashboardRouteScope>
function createDashboardRouteScope(parent: AppContainer) {
return parent.createScope({ route: { routeName: 'dashboard' } })
}
const RootDIKey: InjectionKey<AppContainer> = Symbol('InferDI.root')
const RouteDIKey: InjectionKey<RouteContainer> = Symbol('InferDI.route')
export const AppDIProvider = defineComponent({
setup(_props, { slots }) {
provide(RootDIKey, root)
return () => slots.default?.()
}
})
export const DashboardRouteProvider = defineComponent({
setup(_props, { slots }) {
const parent = inject(RootDIKey)
if (parent === undefined) throw new Error('DI provider is missing')
const scope = createDashboardRouteScope(parent)
provide(RouteDIKey, scope)
// Vue unmount hooks are synchronous; call async dispose explicitly
onUnmounted(() => {
scope.dispose().catch(console.error)
})
return () => slots.default?.()
}
})
export function useDashboardViewModel() {
const container = inject(RouteDIKey)
if (container === undefined) throw new Error('Dashboard route scope is missing')
return container.get('dashboardVm')
}Fichier du dépôt : examples/frontend/vue.ts
Svelte
ts
import { Container } from '@inferdi/inferdi'
import { getContext, onDestroy, setContext } from 'svelte'
type RouteContext = {
readonly routeName: string
}
class ApiClient {
async loadInbox() {
return [{ id: 'msg_1', subject: 'Hello' }]
}
}
class InboxViewModel {
constructor(
private readonly route: RouteContext,
private readonly api: ApiClient
) {}
async load() {
return {
route: this.route.routeName,
messages: await this.api.loadInbox()
}
}
}
const root = new Container()
.declareScopeInputs<{ route: RouteContext }>()
.registerClass('api', ApiClient, [])
.registerClass('inboxVm', InboxViewModel, ['route', 'api'], 'scoped')
type AppContainer = typeof root
type RouteContainer = ReturnType<typeof createInboxRouteScope>
const RootDIKey = Symbol('InferDI.root')
const RouteDIKey = Symbol('InferDI.route')
function createInboxRouteScope(parent: AppContainer) {
return parent.createScope({ route: { routeName: 'inbox' } })
}
export function provideRootContainer() {
setContext(RootDIKey, root)
}
export function provideInboxRouteScope() {
const parent = getContext<AppContainer | undefined>(RootDIKey)
if (parent === undefined) {
throw new Error('InferDI root context is missing — call provideRootContainer() in a parent layout')
}
const scope = createInboxRouteScope(parent)
setContext(RouteDIKey, scope)
// Svelte onDestroy is synchronous; start async cleanup and report failures
onDestroy(() => {
scope.dispose().catch(console.error)
})
}
export function getInboxViewModel() {
const scope = getContext<RouteContainer | undefined>(RouteDIKey)
if (scope === undefined) {
throw new Error('Inbox route scope is missing — call provideInboxRouteScope() in this route')
}
return scope.get('inboxVm')
}Fichier du dépôt : examples/frontend/svelte.ts
