Iti Dependency Injection

repository·master·Indexed 41 days ago

https://github.com/molszanski/iti

A 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.

Tokens
14.4K
Snippets
48
Records
53
Agent score
52%

What's inside Iti

  1. Overview of Iti dependency injection

    master
    Iti is a lightweight (under 1kB) dependency injection (DI) framework designed for TypeScript and JavaScript. It focuses on simplicity, type safety, and native support for asynchronous dependency flows. Unlike traditional IoC libraries, Iti does not require decorators, reflect-metadata, or complex Babel configurations, making it compatible with modern build tools like Vite, Next.js, and Create React App without extra setup.
  2. Overview of ITI features

    master

    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.

    Key Features

    • Async Support: Merges async code and constructor injection using plain async functions.
    • Strongly Typed: Provides IDE autocomplete and compile-time checks without manual type casting.
    • Non-invasive: Business logic does not require imported decorators or framework-specific class extensions.
    • Lazy Initialization: App modules and containers are initialized on demand.
    • Dynamic Imports: Supports split chunks and dynamic imports via a fully async core.
    • React Friendly: Includes React bindings to separate business logic from the view layer.
    • Zero Build Config Hassle: Does not require reflect-metadata or decorators, meaning no special Babel or TypeScript configuration (like experimentalDecorators) is needed.
  3. Retrieve multiple containers using `getItems`

    master

    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:

    • Array of keys: Passing an array of string identifiers.
    • Selector function: Passing a function that takes the container and returns an array of desired containers.
    • Destructuring selector: Passing a function that destructures specific containers from the input and returns an object of them.
    • Token access: Accessing containers via the 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])
  4. Understand the Starlight project structure

    master

    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.json
  5. Key features of Iti

    master

    Iti provides several advantages for developers looking for a modern DI solution:

    • Async Support: Merges asynchronous code with constructor injection using plain async functions.
    • Strong Typing: Provides full IDE autocomplete and compile-time checks without manual type casting.
    • Non-invasive: Your business logic does not need to import decorators or extend framework-specific classes.
    • Lazy Initialization: Modules and containers are initialized on demand.
    • Dynamic Imports: Supports split chunks and dynamic imports via its async core.
    • React Integration: Includes dedicated React bindings (iti-react) to separate business logic from the view layer.
    • Zero Config: No reflect-metadata or decorator configuration required in your build pipeline.
  6. Use Dynamic Imports for split chunks

    master

    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.oven
  7. Implement Singleton and Transient lifecycles

    master

    Iti 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") // false
  8. Compare Pure DI vs ITI for dependency injection

    master

    When 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:

    • Pure DI: You manually instantiate every dependency and pass them through constructors. This can become verbose as the dependency graph grows.
    • ITI: You use a container to define how dependencies are created. You register providers using .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()
  9. Configure ITI with React

    master

    To use ITI in a React application, follow a three-step pattern:

    1. Create a Container: Define your data fetching logic using createContainer() from iti.
    2. Provide Context: Create a 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.
    3. Generate Hooks: Use 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()
      // ...
    }
  10. Optimize async node searches with lookup tables in `addPromise`

    master

    To 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
      )