npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

reduct

v3.3.1

Published

Functional Dependency Injection (DI) for JavaScript

Downloads

131

Readme

Reduct npm travis codecov

Functional Dependency Injection (DI) for JavaScript

Reduct is a simple (<100 lines) caching, functional dependency injector. It lets you eliminate a lot of boilerplate from your code, simplifies mocking (without ugly require hacks) and allows you to run multiple instances of your app in the same process.

Trivia: Reduct was inspired by Redux' simplistic, functional approach.

What is Functional Dependency Injection?

Functional Dependency Injection is when a constructor (or factory) accepts an injector function as its only argument. It's easiest to explain by example:

const Log = require('./log')
const DB = require('./db')

class Router {
  constructor (deps) {
    this.log = deps(Log)
    this.db = deps(DB)
  }
}

Basic injector

To instantiate a class like that, we need an injector. The simplest injector is:

const injector = Constructor => new Constructor(injector)

You can already use it to instantiate entire trees of classes:

const router = new Router(injector)

But as-is, it won't cache any instances. So let's memoize it:

import memoize from 'lodash/memoize'

// Use ES6 Map to ensure keys meet strict equality (rather than string coercion)
memoize.Cache = Map
const injector = memoize(Constructor => new Constructor(injector))

That's it. reduct has a few more features than this minimalist injector which are described below.

Usage

Creating an injector

If you call reduct() without arguments, reduct will return a new injector with an empty cache.

import reduct from 'reduct'

const injector = reduct()

Constructing objects

If you call reduct()(A), reduct will create a new injector and construct an instance of A. Whenever an injector constructs an object, it passes itself as the first and only parameter. In this case, there is no constructor, so the object is simply instantiated and returned.

import reduct from 'reduct'

class A {}

const a = reduct()(A)
console.log(a instanceof A) // => true

When an class does have dependencies, it's easy to retrieve them by calling the injector (usually called deps). The injector will automatically create instances of any classes that it hasn't instantiated before.

import reduct from 'reduct'

class A {}
class B {
  constructor (deps) {
    const a = deps(A)

    console.log(a instanceof A) // => true
  }
}

const b = reduct()(B)
console.log(b instance of B) // => true

Override classes

For testing it's often useful to override a specific class with another (mock) class. This can be achieved by calling the setOverride method on the injector.

For example:

import reduct from 'reduct'

// Your app code
class Database {}

class App {
  constructor (deps) {
    this.database = deps(Database)
  }
}

// Your test code
class MockDatabase {}

const deps = reduct()
deps.setOverride(Database, MockDatabase)
const app = deps(App)

console.log(app.database.constructor.name) // => 'MockDatabase'

Note that override classes can have dependencies like any other class.

Parent injectors

You can pass another injector to reduct which will be consulted first. Only if this parent injector returns a falsy value will reduct attempt to construct the object itself.

Here is an example of a parent injector:

const app = reduct(Constructor => console.log(Constructor.name))(App)

Since the parent injector will always be called when reduct tries to instantiate a class and because console.log always returns undefined, this will simply log all classes' names before instantiation.

As a convenience, reduct will automatically convert Maps and objects into injector functions:

const app = reduct({ Database: CustomDatabase })(App)

This is great for mocking/overriding specific classes in unit tests.

Note: Objects use string keys, meaning the above will override all classes with the name 'Database'. It's also brittle and will break when minified. If you want to override a class by reference instead of by name, use a Map:

const depMap = new Map()
depMap.set(Database, new MockDatabase())
const app = reduct(depMap)(App)

Using reduct as a default injector

When writing a library you can easily use reduct as a default injector:

import reduct from 'reduct'

class Library {
  constructor (deps) {
    deps = deps || reduct()
  }
}

Tip: reduct() will return a new injector with an empty cache.

Module not required in libraries

If you are writing a library and you are comfortable exporting the injector pattern, you don't need to depend on reduct at all:

import Database from './database'

class AwesomeLibraryClass {
  constructor (deps) {
    this.db = deps(Database)
  }
}

Circular references

Dependency injection makes circular references more explicit and therefore safer than vanilla ES6 or CommonJS.

If two classes depend on each other, you can define a method to be executed right after the constructor. Any class you depend on within the post-constructor can be successfully instantiated even if it depends on your class, because your class is already cached at this point.

You can register a post-constructor using deps.later. Post-constructors will be executed synchronously immediately after the constructor and before your instance is returned to whoever requested it.

Caution: Other classes may interact with your class in their constructor (or post-constructor) before your post-constructor has completed, but only in their constructor and only if your post-constructor directly or indirectly depended on them.

class A {
  constructor (deps) {
    deps.later(() => {
      this.b = deps(B)
    })
  }
}

class B {
  constructor (deps) {
    deps.later(() => {
      this.a = deps(A)
    })
  }
}

const a = reduct()(A)
console.log(a === a.b.a) // => true

Tip: You could use this feature to create database models that have mutual relations.

Caution: You can have two classes depend on each other symmetrically like the example above or you can have only one of them use a post-constructor. However, in the asymmetric case, the class without a post-constructor must be instantiated last.

Migrating from an older version

Please see MIGRATING.md.