mimittqq / redi

πŸ’‰ A dependency injection library for TypeScript & JavaScript, along with a binding for React.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

redi

A dependency library for TypeScript and JavaScript, along with a binding for React.

Demo TodoMVC | Demo Repo

Codecov

Features

redi (pronounced 'ready') is a dependency injection library for TypeScript (& JavaScript with some babel config). It also provides a set of bindings to let you adopt the pattern in your React applications.

  • Completely opt-in. Unlike Angular, redi let you decide when and where to use dependency injection.
  • Hierarchical dependency tree.
  • Supports multi kinds of dependency items, including
    • classes
    • instances
    • factories
    • async items
  • Supports n-ary dependencies
    • Required
    • Optional
    • Many
  • Constructor dependencies.
  • Forward ref, to resolve problems rising from cyclic dependency of JavaScript files.
  • Lazy instantiation, instantiate a dependency only when they are accessed to boost up performance.

Getting Started

Installation

npm install @wendellhu/redi

After installation you need to enable experimentalDecorators in your tsconfig.json file.

{
    "compilerOptions": {
+       "experimentalDecorators": true
    }
}

Basics

Let's get started with a real-word example:

class AuthService {
    static public getCurrentUserInfo(): UserInfo {
        // your implementation here...
    }
}

class FileListService {
    constructor() {}

    public getUserFiles(): Promise<Files> {
        const currentUser = // ...AuthService.getCurrentUserInfo()
        // ...
    }
}

It is clearly that FileListServices dependents on AuthService, so you just need to declare it on the constructor of FileListService.

Step 1. Declare dependency relationship.

class AuthService {
    public getCurrentUserInfo(): UserInfo {
        // your implementation here...
    }
}

+ import { Inject } from '@wendellhu/redi'

class FileListService {
-   constructor() {}
+   constructor(@Inject(AuthService) private readonly authService: AuthService) {}

    public getUserFiles(): Promise<Files> {
-       const currentUser = // ...AuthService.getCurrentUserInfo()
+       const currentUser = this.authService.getCurrentUserInfo()
        // ...
    }
}

Then you need to include all things into an Injector.

Step 2. Provide dependencies.

import { Injector } from '@wendellhu/redi'

const injector = new Injector([[FileListService], [AuthService]])

You don't instantiate a FileListService by yourself. You get a FileListService from the injector just created.

Step 3. Wire up!

const fileListService = injector.get(FileListService)

That's it!

React Bindings

redi provides a set of React bindings in it's secondary entry point @wendellhu/redi/react-bindings that can help you use it in your React application easily.

import { withDependencies } from '@wendellhu/redi/react-bindings'

const App = withDependencies(
    function AppImpl() {
        const injector = useInjector()
        const fileListService = injector.get(FileListService)
        // ...
    },
    [[FileListService], [AuthService]]
)

Concepts

  • The injector holds a set of bindings and resolves dependencies.
  • A binding maps a token to a dependency item.
    • Token works as an identifier. It differentiate a dependency from another. It could be the return value of createIdentifier, or a class.
    • Dependency could be
      • a class
      • an instance or value
      • a factory function
      • an async item, which would be resoled to an other kind of dependency later
  • Dependency could declare its own dependencies, and contains extra information on how its dependencies should be injected, and contains extra information on how its dependencies should be injected.

API

Decorators

createIdentifier

function createIdentifier<T>(id: string): IdentifierDecorator<T>

Create a token that could identify a dependency. The token could be used as an decorator to declare dependencies.

import { createIdentifier } from '@wendellhu/redi'

interface IPlatformService {
    copy(): Promise<boolean>
}

const IPlatformService = createIdentifier<IPlatformService>()

class Editor {
    constructor(@IPlatformService private readonly ipfs: IPlatformService) {}
}

Inject Many Optional

  • Inject marks the parameter as being a required dependency. By default, token returned from createIdentifier marks the parameter as required as well.
  • Many marks the parameter and being a n-ary dependency.
  • Optional marks the parameter as being an optional dependency.
class MobileEditor {
    constructor(
        @Inject(SoftKeyboard) private readonly softKeyboard: SoftKeyboard,
        @Many(Menu) private readonly menus: Menu[],
        @Optional(IPlatformService) private readonly ipfs?: IPlatformService
    ) {}
}

Self SkipSelf

  • Self marks that the parameter should only be resolved by the current injector.
  • SkipSelf marks that parameter should be resolved from the current injector's parent.
import { Self, SkipSelf } from '@wendellhu/redi'

class Person {
    constructor() {
        @Self() @Inject(forwardRef(() => Father)) private readonly father: Father,
        @SkipSelf() @Inject(forwardRef(() => Father)) private readonly grandfather: Father
    }
}

class Father extends Person {}

Dependency Items

ClassItem

interface ClassDependencyItem<T> {
    useClass: Ctor<T>
    lazy?: boolean
}
  • useClass the class
  • lazy enable lazy instantiation. The dependency would be instantiated only when CPU is idle or its properties or methods are actually accessed.

ValueDependencyItem

export interface ValueDependencyItem<T> {
    useValue: T
}

FactoryDependencyItem

export interface FactoryDependencyItem<T> {
    useFactory: (...deps: any[]) => T
    deps?: FactoryDep<any>[]
}

AsyncDependencyItem

export type SyncDependencyItem<T> =
    | ClassDependencyItem<T>
    | FactoryDependencyItem<T>
    | ValueDependencyItem<T>

interface AsyncDependencyItem<T> {
    useAsync: () => Promise<
        T | Ctor<T> | [DependencyIdentifier<T>, SyncDependencyItem<T>]
    >
}

Injector

class Injector {
    constructor(collectionOrDependencies?: Dependency[], parent?: Injector) {}
}

Create an injector with a bunch of bindings.

You can pass in another Injector as its parent injector.

class Injector {
    public createChild(dependencies?: Dependency[]): Injector
}

Create a child injector. When a child injector could not resolve a dependency, it would delegate to its parent injector.

class Injector {
    public dispose(): void
}

Dispose an injector, its child injectors and all disposable dependencies in the injector tree.

class Injector {
    public add<T>(ctor: Ctor<T>): void
    public add<T>(
        id: DependencyIdentifier<T>,
        item: DependencyItem<T> | T
    ): void
    public add<T>(
        idOrCtor: Ctor<T> | DependencyIdentifier<T>,
        item?: DependencyItem<T> | T
    ): void
}

Add a dependency or a value into the injector.

class Injector {
    public get<T>(id: DependencyIdentifier<T>, lookUp?: LookUp): T
    public get<T>(
        id: DependencyIdentifier<T>,
        quantity: Quantity.MANY,
        lookUp?: LookUp
    ): T[]
    public get<T>(
        id: DependencyIdentifier<T>,
        quantity: Quantity.OPTIONAL,
        lookUp?: LookUp
    ): T | null
    public get<T>(
        id: DependencyIdentifier<T>,
        quantity: Quantity.REQUIRED,
        lookUp?: LookUp
    ): T
    public get<T>(
        id: DependencyIdentifier<T>,
        quantity: Quantity,
        lookUp?: LookUp
    ): T
    public get<T>(
        id: DependencyIdentifier<T>,
        quantityOrLookup?: Quantity | LookUp,
        lookUp?: LookUp
    ): T[] | T | null
}

Get a dependency from the injector.

class Injector {
    public getAsync<T>(id: DependencyIdentifier<T>): Promise<T>
}

Get an async dependency.

class Injector {
    public createInstance<T extends unknown[], U extends unknown[], C>(
        ctor: new (...args: [...T, ...U]) => C,
        ...customArgs: T
    ): C
}

Instantiate a class-type dependency with extra parameters.

forwardRef

In the example above, Person is declared before Father, but it depends on Father. In this case, you need to use forwardRef to wrap Father. Otherwise, Father is evaluated to undefined in dependency relationship resolution.

import { Self, SkipSelf } from '@wendellhu/redi'

class Person {
    constructor() {
        @Self() @Inject(forwardRef(() => Father)) private readonly father: Father,
        @SkipSelf() @Inject(forwardRef(() => Father)) private readonly grandfather: Father
    }
}

class Father extends Person {}

Singletons

Sometimes you want some dependencies to be singletons. In that case, you don't have to add them to the root injector manually. Instead, you can just use registerSingleton.

export function registerSingleton<T>(
    id: DependencyIdentifier<T>,
    item: DependencyItem<T>
): void

Singletons would be fetched by the root injectors (in another word, injectors that don't have a parent injector) automatically.

In avoidance of unexpected error, it is strongly recommended to have only one root injector in your application.

React Bindings

connectDependencies

export function connectDependencies<T>(
    Comp: React.ComponentType<T>,
    dependencies: Dependency[]
): React.ComponentType<T>

Bind dependencies into a React component. The dependencies would be instantiated when they are used in the React component tree. When you wrap a connected React component inside another, the injectors will hook up as well.

React Context

export const RediProvider = RediContext.Provider
export const RediConsumer = RediContext.Consumer

React context to consume or provide an Injector. In most cases you don't have to use them.

Hooks

export function useInjector(): Injector

Get the nearest Injector.

Decorators

export function WithDependency<T>(
    id: DependencyIdentifier<T>,
    quantity?: Quantity,
    lookUp?: LookUp
): any

A decorator to be used on Class Component to get a dependency from the nearest Injector. An example:

class AppImpl extends React.Component<{}> {
    static contextType = RediContext

    @WithDependency(IPlatformDependency)
    private readonly platform!: IPlatformDependency

    render() {
        return <div>{this.a.key}</div>
    }
}

JavaScript

Redi could also be used in your JavaScript projects, provided that you use Babel to transpile your source files. Just add this babel plugin to your babel config.

License

MIT. Copyright 2021 Wendell Hu.

About

πŸ’‰ A dependency injection library for TypeScript & JavaScript, along with a binding for React.

License:MIT License


Languages

Language:TypeScript 98.7%Language:JavaScript 1.3%