needle-inject
TypeScript icon, indicating that this package has built-in type declarations

1.2.3 • Public • Published

Needle

npm repo

Seamless dependency injection for Typescript

Installation

From npm:

npm i needle-inject

Usage

Important: When using the decorator, make sure you have the flags emitDecoratorMetadata and experimentalDecorators both set to true in tsconfig.json

{
  "emitDecoratorMetadata": true,
  "experimentalDecorators": true
}

Any class can be used as a singleton using needle, no extra decorations allowing for backwards compatiblity.

Examples

Usage should be pretty straightforward, we have a service class we want to inject into another class/function. To achieve this we do not have to touch the original class simply make sure it has a constructor that takes no arguments.

class FooService {
    
    procedure(obj : number) : boolean {
        // do complex logic
        return obj % 2 != 0 
    }

    requestThing() : Promise<any> {
        // Send async call
    }
    
}

To use FooService:

  • Using function injection:

    import { provide } from "needle-inject"
    import FooService from "./FooService"
    
    const service = provide(FooService)
    console.log(service.procedure(2))
  • With class injection:

    import { inject } from "needle-inject"
    import FooService from "./FooService"
    
    class Goopus {
        
        @inject
        private fooService : FooService;
        
        constructor(obj : number) {
            console.log(fooService.procedure(obj))
        }
        
    }
    
    let goopus = new Goopus(3)
    console.log(goopus.fooService.procedure(6))
  • Using auto injection:

    import { autoInject, create } from "needle-inject"
    import FooService from "./FooService"
    
    @autoInject
    class Bloopus {
     
      constructor(private fooService : FooService) {
        
      }
     
      evaluate(obj : number) {
          console.log(this.fooService.procedure(obj))
      }
     
    }
    
    let bloopus = create(Bloopus);
    bloopus.evaluate(13)

These run in succesion will produce:

false
true
false
true

With React

Here is an example component that uses FooService:

import * as React from "react";
import { inject } from "needle-inject"
import FooService from "./FooService"

export default class MyComponent extends React.Component {

    @inject
    private fooService : FooService;

    render() {
        return <div>
            <Button onClick={this.fooService.requestThing()}>Send Request</Button>
            <p>
                Procedure of 3 is {this.fooService.procedure(3)}
            </p>
        </div>
    }

}

Use case with React & MobX

You can run an example app located in examples/react-mobx. Run npm start and navigate to localhost:1234 to view it.


When managing state in React.js a lot of the time you want a reference to a global store. When using the very useful library MobX they provide a very unopinionated way to handle the passing of stateful objects. Here is where needle becomes useful!

  1. Create a store class that will store state:

    import { observable, computed } from "mobx"
    
    // I store the state for auth sessions
    class AuthStore {
        @observable userToken : String | null;
        @observable userSession : any;
    
        @computed get isLoggedIn() {
            return this.userToken != null;
        }
    }
  2. Create a service that will hold this store:

    import { action } from "mobx";
    
    export default class AuthService {
    
        store: AuthStore;
    
        constructor() {
            store = new AuthStore();
        }
    
        @action
        logIn() {
            // Do complex logic here...
            this.store.userToken = "TOKEN_FROM_AUTH";
        }
    
    }
  3. Now we can inject AuthService, observe that store and any changes to that state will update our components:

    import * as React from "react";
    import { observer } from "mobx-react";
    import { inject } from "needle-inject";
    import AuthService from "./AuthService";
    
    @observe
    export default class MyComponent extends React.Component {
    
        @inject
        private authService : AuthService;
    
        render() {
            return this.authService.isLoggedIn ? <div>I am logged in!</div> : <a href="/login">Login?</a>;
        }
    
    }

Usage with Babel 7

Babel 7 includes it's own Typescript parser but unfortunately, from what I could find, it does not support emitting metadata that is required for the @inject annotation to function.

My suggested workaround is to pre-transpile using tsc (ts-loader with webpack etc.) and then feeding the result into Babel 7 until they support this feature.

Next.js and Babel 7

To mitigate the fact that next.js uses Babel 7 there is an addon for next.config.js bundled. To use needle & typescript with next.js:

const addons = require("needle-inject/dist/addons");
const {withNeedle} = addons.next;

module.exports = withNeedle({})

How it works

There isn't actually much going on under the hood. We keep a singleton InjectionManager which stores all the current mappings. When a class instance is requested we check if we already have one and serve it, otherwise creating a new instance and storing it for next time.

Limitations

  • I've yet to explore what happens when multiple libraries use needle at the same time. The problem I see arisng is two libraries using two impls of the same base class. Let's say they share a common lib with AuthService as an abstract base class yet they both use seperate services that inherit from this. One will clash with the other.

  • More complicated functionality has not yet being needed as part of my requirements but when they arise I fear they will complicate the current minimalist impl. Overthinking it? Probably.

Readme

Keywords

Package Sidebar

Install

npm i needle-inject

Weekly Downloads

3

Version

1.2.3

License

ISC

Unpacked Size

32.5 kB

Total Files

36

Last publish

Collaborators

  • censkh