Skip to content

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.

ExempleContenu
react.tsxScope de fonctionnalité React géré par @inferdi/react
react-native.tsxScope d’écran React Native
vue.tsLimite de scope avec provide/inject de Vue 3
svelte.tsLimite 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