Skip to content

Frontend Frameworks

Frontend examples create scopes at page, route, screen, or feature boundaries. They keep their own small builders instead of importing the server-side shared module.

Compare where each framework creates the scope, how the scope is provided to children, and how cleanup runs on unmount.

ExampleShows
react.tsxReact feature scope managed by @inferdi/react
react-native.tsxReact Native screen scope
vue.tsVue 3 provide/inject scope boundary
svelte.tsSvelte context scope boundary

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')
}

Repository file: 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')
}

Repository file: 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')
}

Repository file: 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')
}

Repository file: examples/frontend/svelte.ts