Frontend-Frameworks
Frontend-Beispiele öffnen Scopes für Seiten, Routen, Screens oder Features. Sie verwenden eigene kleine Builder statt des gemeinsamen Servermoduls.
Vergleiche, wo der Scope erstellt wird, wie Kindkomponenten darauf zugreifen und wie die Freigabe beim Entfernen erfolgt.
| Beispiel | Inhalt |
|---|---|
react.tsx | Von @inferdi/react verwalteter Feature-Scope |
react-native.tsx | Scope für einen React-Native-Screen |
vue.ts | Scope-Grenze über provide/inject in Vue 3 |
svelte.ts | Scope-Grenze über den Svelte-Kontext |
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')
}Datei im Repository: 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')
}Datei im Repository: 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')
}Datei im Repository: 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')
}Datei im Repository: examples/frontend/svelte.ts
