DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Android Cloud Apps with Azure
  • Delivering Your Code to the Cloud With JFrog Artifactory and GitHub Actions
  • Migrating Spring Java Applications to Azure App Service (Part 1: DataSources and Credentials)
  • Visually Designing Views for Java Web Apps

Trending

  • Securing the Future: Best Practices for Privacy and Data Governance in LLMOps
  • Driving DevOps With Smart, Scalable Testing
  • Memory Leak Due to Time-Taking finalize() Method
  • Introduction to Retrieval Augmented Generation (RAG)
  1. DZone
  2. Software Design and Architecture
  3. Cloud Architecture
  4. Preact With InversifyJS for Dependency Injection

Preact With InversifyJS for Dependency Injection

Integrate dependency injection to your Preact application. Write readable and less code with services directly injected into your Components and Services.

By 
Haritha Yahathugoda user avatar
Haritha Yahathugoda
·
Updated Jan. 25, 21 · Tutorial
Likes (5)
Comment
Save
Tweet
Share
7.8K Views

Join the DZone community and get the full member experience.

Join For Free

Introduction

Preact is a lightweight front-end framework that provides the most useful features from the React framework in a very small, ~3KB, package. One thing that is missing from Preact, or ReactJS, is a mechanism to dependency inject (DI) services. InversifyJS provides a lightweight and powerful Inversion of Control (IoC) framework that allows us to inject dependencies to our Components and services using decorators. Using InversifyJS on a Preact application, we can reduce the boilerplate code needed for instantiating services.

You can download the completed application at this GitHub repo.

 Screenshot of the application we will be building to understand how to use DI in a JS application

Audience

Preferably, the reader should have some experience in TypeScript. Someone with experience in JavaScript and the NodeJS ecosystem can follow this tutorial with some help from Google.

Abstract Lesson

I will create a simple TODO list app as an example to show how to use and benefit from dependency injection. The example app is mainly composed of the following two services and some TypeScript and HTML markup to use them.

  1. TODOListService which manages items on the to-do list.
  2. StorageService which saves to-do list items to localStorage. 

And some HTML markup. Key points to look out for in the proceeding article:

  1. How we use the @injectable() decorator in our service classes to let the application know that the class should be added to the list of providers that can be injected.
  2. Use of @inject() on the constructor of the TODOListService service to inject dependencies without explicitly calling new StorageService().
  3. How we build a @lazyInject() decorator to inject services to our Preact Component at runtime.

At the end of this article, you will be able to:

  1. Create a Preact application.
  2. Use InversifyJS to inject services into your components and services.

Set Up a Preact Project With TypeScript

Create a directory for your project and cd into it:

Shell
 




x


 
1
mkdir preact-with-inversifyjs-as-di
2
cd preact-with-inversifyjs-as-di



Install Preact CLI globally. This allows us to use it on the terminal. I recommend using NVM to create a new environment and install it there, but for this tutorial, I will install it globally.

Shell
 




xxxxxxxxxx
1


 
1
npm install -g preact-cli



Create a Preact app with TypeScript. Make sure you have NodeJS 12 LTS+ installed on your machine.

Shell
 




xxxxxxxxxx
1


 
1
preact create typescript webapp



Important: webapp directory is the root directory of our project. All the paths mentioned in this article are relative to this folder.

The above command will create a boilerplate Preact web app with TypeScript in the webapp directory. Open your tsconfig.json files and uncomment the lines containing experimentalDecorators and emitDecoratorMetadata. Also, set the strict flat to false.

Let's clean up the application so that we are only left with the application entry class, app.tsx.

  1. Delete all the folders inside routes folder.
  2. Delete components/header folder.
  3. Delete tests/header.test.tsx file.

Now, we need to clean up ourappcomponent. The following is a cleaned-up version of app.tsx using class implementation:

TypeScript
 




xxxxxxxxxx
1
11


 
1
import { Component, h } from "preact"; 
2

          
3
export default class App extends Component {
4
    render() {
5
        return (
6
            <div id="app">
7
                Hello World!
8
            </div>
9
        );
10
    }
11
 }



On package.json, update the serve and dev scripts so that we can access our application at localhost:3001

Properties files
 




xxxxxxxxxx
1


 
1
...
2
"scripts" : { 
3
...
4
    "serve": "sirv build --port 3001 --cors --single",
5
    "dev": "preact watch --port 3001", 
6
...
7
}



Now if you run npm run dev and visit localhost:3001 on a browser, it should display the text Hello World! on the page.

Set Up InversifyJS for Dependency Injection

First, we need to install the required packages.

Install InversifyJS and inject decorator:

Shell
 




xxxxxxxxxx
1


 
1
npm install reflect-metadata inversify inversify-inject-decorators 
2
npm install -D babel-plugin-transform-typescript-metadata babel-plugin-parameter-decorator



Thereflect-metadatapackage allows our decorators on classes and properties to emit their metadata.

inversify andinversify-inject-decoratorsgive us decorators and a container that manager our injectables.

And the last two dev dependencies are added so that we can inject services into other services by passing them as arguments to their constructors with the @inject decorator.

Now that we have everything installed. Let's modify the src/.babelrc file to support adding decorators to the constructor. Replace the content of .bablerc with the following.

JSON
 




xxxxxxxxxx
1
11


 
1
{
2
    "plugins": [
3
        // https://github.com/inversify/InversifyJS/issues/1004#issuecomment-642118301
4
        // Fixes @inject on constructor
5
        "babel-plugin-transform-typescript-metadata",
6
        "babel-plugin-parameter-decorator"
7
    ],
8
    "presets": [
9
        "preact-cli/babel"
10
    ]
11
}



Then we need to tell Preact to use our.babelrcfile for building the App. Open thepreact.config.jsand add the following code as the first line of code on thewebpack(...){ function.

Properties files
 




xxxxxxxxxx
1


 
1
config.module.rules[0].options.babelrc = true;



For all of this to work, we need to add the relect-metadata package to our project. Add the following line as the first line in your index.js file.

TypeScript
 




xxxxxxxxxx
1


 
1
import 'reflect-metadata';



Initialize Our Service Container and Provider

First of all, we need a container to which we can register our services.

Initialize the container. Create a file di/services.container.ts and add the following content:

TypeScript
 




xxxxxxxxxx
1
22


 
1
import { Container } from 'inversify';
2
import getDecorators from 'inversify-inject-decorators'; 
3

          
4
// Must have https://www.npmjs.com/package/babel-plugin-transform-typescript-metadata
5
// setup the container...
6
export const container = new Container({ autoBindInjectable: true }); 
7

          
8
// Additional function to make property decorators compatible with babel.
9
let { lazyInject: originalLazyInject } = getDecorators(container); 
10

          
11
function fixPropertyDecorator<T extends Function>(decorator: T): T {
12
    return ((...args: any[]) => (
13
        target: any,
14
        propertyName: any,
15
        ...decoratorArgs: any[]
16
    ) => {
17
        decorator(...args)(target, propertyName, ...decoratorArgs);
18
        return Object.getOwnPropertyDescriptor(target, propertyName);
19
    }) as any;
20
}
21

          
22
export const lazyInject = fixPropertyDecorator(originalLazyInject);



Besides initializing the container, we are also overriding thelazyInjectdecorator here. Our transform plugin for babel has some issues when@injectis used with properties. To mitigate that we need to override the defaultlazyInjectas mentioned on https://github.com/leonardfactory/babel-plugin-transform-typescript-metadata/issues/2#issuecomment-477130801.

Great! now we can create injectable services and inject them anywhere in our application.

Create StorageService

TypeScript
 




xxxxxxxxxx
1
32


 
1
import { injectable } from 'inversify'; 
2

          
3
@injectable()
4
export default class StorageService {
5
    _inMemoryStorage: { [key: string]: any } = {};
6
    store(key: string, value: any) {
7
        this.getLocalStorage().setItem(key, JSON.stringify(value))
8
    }
9

          
10
    getAsJSON(key: string) {
11
        const storedData = this.getLocalStorage().getItem(key);
12
        return storedData ? JSON.parse(storedData) : null;
13
    }
14

          
15
     getLocalStorage() {
16
        if (globalThis['localStorage']) {
17
            return localStorage;
18
        }
19
        return this.getInMemoryStorage();
20
    }
21

          
22
     getInMemoryStorage() {
23
        return {
24
            setItem: (key: string, value: any) => {
25
                this._inMemoryStorage[key] = value;
26
            },
27
            getItem: (key: string) => {
28
                return this._inMemoryStorage[key];
29
            }
30
        };
31
    }
32
}



Note: I added getLocalStorage and getInMemoryStorage so that this Service work with server-side rendering.

We need to register this with our container in services.container.ts. Add the following line right after the export const container... line:

TypeScript
 




xxxxxxxxxx
1


 
1
container.bind('StorageService').to(StorageService).inSingletonScope();



This tells the container to bind the name StorageService string to the StorageService class in singleton scope. Singleton scope will make sure that there will be only one instance of StorageService available to the container. This is great since we only need one instance of StorageService to access the storage.

Create TODOListService

TypeScript
 




xxxxxxxxxx
1
41


 
1
import { inject, injectable } from 'inversify';
2
import StorageService from './storage.service'; 
3

          
4
const TODO_LIST_STORAGE_KEY = 'TODO_LIST_STORAGE_KEY';
5

          
6
// Event type to listen for changes to the TODO list
7
export const LIST_CHANGED_EVENT_TYPE_ID = 'list-changed';
8

          
9
@injectable()
10
export default class TODOListService {
11
     constructor(
12
        @inject('StorageService') private storageService: StorageService,
13
    ) {
14
    }
15

          
16
    addListItem(item: string) {
17
        let todoList = this.storageService.getAsJSON(TODO_LIST_STORAGE_KEY);
18
        todoList = todoList || [];
19
        todoList.push(item);
20
        this.storageService.store(TODO_LIST_STORAGE_KEY, todoList);
21
        this.notifyListChanged();
22
    }
23

          
24
    getListItems() {
25
        let todoList = this.storageService.getAsJSON(TODO_LIST_STORAGE_KEY);
26
        todoList = todoList || [];
27
        return todoList;
28
    }
29

          
30
    getItemCount() {
31
        let todoList = this.storageService.getAsJSON(TODO_LIST_STORAGE_KEY);
32
        todoList = todoList || [];
33
        return todoList.length;
34
    }
35

          
36
     // In practice, this should a Subject to which others can subscribe.
37
     // But for the purpose of this article, I will just emit this as an event
38
    notifyListChanged() {
39
        window.dispatchEvent(new Event(LIST_CHANGED_EVENT_TYPE_ID))
40
    }
41
 }



Register this service with the container the same way you added StorageService

TypeScript
 




xxxxxxxxxx
1


 
1
container.bind('TODOListService').to(TODOListService).inSingletonScope();



Write the TODO List App

Let's update our app.tsx to the following:

TypeScript
 




xxxxxxxxxx
1
68


 
1
import { Component, createRef, h } from "preact";
2

          
3
import { lazyInject } from '../di/services.container';
4
import TODOListService from '../services/todo-list.service';
5
import { LIST_CHANGED_EVENT_TYPE_ID } from '../services/todo-list.service'; 
6

          
7

          
8
interface AppState {
9
    itemCount: number;
10
    listItems: string[];
11
}
12

          
13

          
14
export default class App extends Component<any, AppState> {
15

          
16
    @lazyInject('TODOListService') todoListService: TODOListService;
17

          
18
     // Creates a reference to our html input element so that we can get its value
19
    inputElement = createRef<HTMLInputElement>();
20

          
21
    constructor() {
22
        super();
23
        this.state = {
24
            itemCount: this.todoListService.getItemCount(),
25
            listItems: this.todoListService.getListItems() || [],
26
        }
27
        // When event an item is added to the todoList, this will be notified of it.
28
        window.addEventListener(LIST_CHANGED_EVENT_TYPE_ID, () => {
29
            this.setState({ itemCount: this.todoListService.getItemCount() });
30
        });
31
         // Update the item list for rendering them on the page
32
        window.addEventListener(LIST_CHANGED_EVENT_TYPE_ID, () => {
33
            this.setState({ listItems: this.todoListService.getListItems() });
34
        });
35
    }
36

          
37
    addAndClearItem($event) {
38
        const item = this.inputElement.current.value;
39
        if (item && item.length > 0) {
40
            this.todoListService.addListItem(item);
41
        }
42
        this.inputElement.current.value = "";
43
    }
44

          
45
    render() {
46
        return (
47
            <div id="app">
48
                <div class="list-item-count">
49
                    You have {this.state.itemCount} items todo.
50
                </div>
51
                <div>
52
                    <div>
53
                        <input ref={this.inputElement} type="text" />
54
                        <button onClick={($event) => this.addAndClearItem($event)}>Add</button>
55
                    </div>
56
                </div>
57
                <div class="todo-list">
58
                    {
59
                        // Render current list
60
                        this.state.listItems.map(item => {
61
                            return <div>{item}</div>
62
                        })
63
                    }
64
                </div>
65
            </div>
66
        );
67
    }
68
}



Lots of code, but this is pretty self-explanatory. The HTMLInputElement let the user type TODO items. Clicking on HTMLButtonElement next to the <input> will call the function addAndClearItem. addAndClearItem call the TODOListService addListItem method to store the list item. Once addListItem stores data, with will dispatch an event to which we listen and render/update the number of items on the list and items on the list.

That is all folks. Now you have an application what inject services to other service and Components. Some can be used to develop any Vue.js or ReactJS application. It is error prune and messy to instantiate classes. As your codebase gets larger, change to a constructor of one service may require you to update multiple files, or worse you might forget to update some files. Which could lead to bugs. Having a centralized place that manages the lifecycle of these services prevent such bugs plus your code will be more concise and readable.

We are using the same DI system to inject dependencies to our PS2PDF online video compress. We inject a similar StorageService that allows us to store uploaded files' metadata and any video compress options, such as target output size, the user chooses. There are stored in a data structure called UploadedFile. When user request to compress the uploaded files, we get that information from the storage service and send to the remote server to process the file. There is a periodic service that polls for the status and adds the status data to UploadedFile on the storage. When UploadedFile state change, we emit a sort of event that we listen to in our Component that renders the file convert status.

Conclusion

Inversion of Control (IoC) is a very powerful concept. It allows you to delegate work to the container instead of writing code to instantiate classes. Dependency Injection (DI) is a form of IoC where we register our providers with a container and the container takes care of managing the lifecycle of the providers from object creation to destruction. Hope this article helps you to understand the usefulness of DI and improve your code quality.

Dependency injection Web Service application TypeScript Container app Inversion of control

Opinions expressed by DZone contributors are their own.

Related

  • Android Cloud Apps with Azure
  • Delivering Your Code to the Cloud With JFrog Artifactory and GitHub Actions
  • Migrating Spring Java Applications to Azure App Service (Part 1: DataSources and Credentials)
  • Visually Designing Views for Java Web Apps

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!