Overview of Iti dependency injection
masterreflect-metadata, or complex Babel configurations, making it compatible with modern build tools like Vite, Next.js, and Create React App without extra setup.repository·master·Indexed 41 days ago
https://github.com/molszanski/itiA lightweight (<1kB) Dependency Injection (IoC) library for TypeScript and React. It provides native support for asynchronous dependency resolution and does not require decorators or reflect-metadata. Includes the iti-react package for integrating containers into React components via hooks like useContainer and useContainerSet.
reflect-metadata, or complex Babel configurations, making it compatible with modern build tools like Vite, Next.js, and Create React App without extra setup.ITI is a tiny (<1kB) typesafe dependency injection framework for TypeScript and JavaScript. It is designed to be non-invasive and supports asynchronous dependency flows.
async functions.reflect-metadata or decorators, meaning no special Babel or TypeScript configuration (like experimentalDecorators) is needed.The getItems method is used to retrieve a set of containers from a parent container. While implementation details vary, the following patterns are explored for selecting which containers to fetch:
cont.tokens property and passing them as an array.// Pattern 1: Array of keys
let containerSet1 = await cont.getItems(["aCont", "bCont", "cCont"])
// Pattern 2: Selector function returning array
let containerSet2 = await cont.getItems((c) => [c.aCont, c.bCont, c.Cont])
// Pattern 3: Destructuring selector returning object
let containerSet3 = await cont.getItems(({ aCont, bCont, cCont }) => ({
aCont,
bCont,
cCont,
}))
// Pattern 4: Using tokens directly
let c = cont.tokens
let containerSet4 = await cont.getItems([c.aCont, c.bCont, c.Cont])A standard Starlight project follows this directory structure:
src/content/docs/: Contains .md or .mdx files. Starlight automatically exposes each file as a route based on its filename.src/assets/: Place images here to embed them in Markdown using relative links.public/: Place static assets like favicons here.astro.config.mjs: The Astro configuration file.src/content.config.ts: Content configuration for Astro..
├── public/
├── src/
│ ├── assets/
│ ├── content/
│ │ └── docs/
│ └── content.config.ts
├── astro.config.mjs
├── package.json
└── tsconfig.jsonIti provides several advantages for developers looking for a modern DI solution:
async functions.iti-react) to separate business logic from the view layer.reflect-metadata or decorator configuration required in your build pipeline.Because Iti's core is fully asynchronous, you can easily implement code-splitting and dynamic imports. You can define a dependency that, when accessed, triggers a dynamic import() of a module. This allows you to load heavy business logic or large modules only when they are actually needed by the application.
// ./kitchen/index.ts
export async function provideKitchenContainer() {
const { Kitchen } = await import("./kitchen/kitchen")
return {
kitchen: () => new Kitchen(),
oven: async () => {
const { Oven } = await import("./kitchen/oven")
const oven = new Oven()
await oven.preheat()
return oven
},
}
}
// ./index.ts
import { createContainer } from "iti"
import { provideKitchenContainer } from "./kitchen"
let cont = createContainer().add({
kitchen: async () => provideKitchenContainer(),
})
// The module ./kitchen/kitchen is loaded here
await cont.items.kitchen
// The module ./kitchen/oven is loaded here
await cont.items.kitchen.ovenIti manages object lifecycles based on how you define your factory functions:
Singleton (Single Instance): By default, providing a factory that returns an instance results in a singleton. The container caches the result of the first call.
Transient:
To create a new instance every time get() is called, provide a factory that returns another function (a factory of a factory).
Note: All async dependencies are also cached by default once resolved.
// Singleton
let cont = createContainer().add({
oven: () => new Oven(),
})
cont.get("oven") === cont.get("oven") // true
// Transient
let cont = createContainer().add({
oven: () => () => new Oven(),
})
cont.get("oven") === cont.get("oven") // falseWhen deciding between Pure DI (manual dependency injection) and ITI, note that your business logic remains identical. ITI does not require framework-specific decorators or extends keywords on your classes or interfaces. The primary difference lies in the Wiring layer:
.add() and access the resolved instances via the container's items property. This centralizes the wiring logic and manages the dependency graph automatically.// Business logic remains pure and unaware of ITI
export interface Logger {
info: (msg: string) => void
}
export class PaymentService {
private readonly logger: Logger
constructor(_logger: Logger) {
this.logger = _logger
}
sendMoney() {
this.logger.info("payment sent")
}
}
// ITI Wiring
const root = createContainer()
.add({
logger: () => new ConsoleLogger(),
})
.add((ctx) => ({
paymentService: () => new PaymentService(ctx.logger),
}))
const ps = root.items.paymentService
ps.sendMoney()You can create a new Astro project using the Starlight template by running the following command with pnpm:
pnpm create astro@latest -- --template starlightTo use ITI in a React application, follow a three-step pattern:
createContainer() from iti.React.Context using the container's type and wrap your application with its Provider, passing an instance of the container created by calling the container function.getContainerHooks() from iti-react to generate typed hooks from your context. These hooks allow components to access container data and loading states.Note on Hook Naming (v0.8.0+):
While getContainerHooks() is the standard way to generate hooks, the returned hook names have been updated for clarity. The following renames are in effect:
useItem (replaces the deprecated useContainer)useItems (replaces the deprecated useContainerSet)import { createContainer } from "iti"
import React from "react"
import { getContainerHooks } from "iti-react"
// 1. Define the container
export const mainApp = () =>
createContainer().add(() => ({
auth: async () => {
const res = await fetch("/api/profile")
return {
profile: res.json(),
}
},
}))
export type MainAppContainer = ReturnType<typeof mainApp>
// 2. Setup Context and Hooks
export const MyAppContext = React.createContext<MainAppContainer>({} as MainAppContainer)
const hooks = getContainerHooks(MyAppContext)
export const useAuth = hooks.useAuth // Access specific items via the generated hooks
// 3. Provide in App
function App() {
const itiContainerInstance = React.useMemo(() => mainApp(), [])
return (
<MyAppContext.Provider value={itiContainerInstance}>
<Profile />
</MyAppContext.Provider>
)
}
// 4. Consume in Component
function Profile() {
const [auth, authErr] = useAuth()
// ...
}To create a new Astro project using the 'basics' template, use the yarn create astro command with the template flag.
yarn create astro@latest -- --template basicsTo improve runtime performance when searching for values within asynchronous nodes, you can provide an array of tokens as a second argument to the addPromise method. This acts as a lookup table, allowing the container to index the keys and perform more efficient, lazy lookups of the returned data.
let n = createContainer()
.addNode({ a: 1, b: 2 })
.addPromise(
async (c) => {
return { c: 3, d: 4 }
},
["c", "d"], // Lookup table for runtime optimization
)