redi
A dependency library for TypeScript and JavaScript, along with a binding for React.
Demo TodoMVC | Demo Repo

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
- 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 {
}
}
class FileListService {
constructor() {}
public getUserFiles(): Promise<Files> {
const currentUser =
}
}
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 classlazy
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.