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.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
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 Component
s 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.
TODOListService
which manages items on the to-do list.StorageService
which saves to-do list items tolocalStorage
.
And some HTML markup. Key points to look out for in the proceeding article:
- 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. - Use of
@inject()
on the constructor of theTODOListService
service to inject dependencies without explicitly callingnew StorageService()
. - How we build a
@lazyInject()
decorator to inject services to our PreactComponent
at runtime.
At the end of this article, you will be able to:
- Create a Preact application.
- 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:
mkdir preact-with-inversifyjs-as-di
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.
xxxxxxxxxx
npm install -g preact-cli
Create a Preact app with TypeScript. Make sure you have NodeJS 12 LTS+ installed on your machine.
xxxxxxxxxx
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
.
- Delete all the folders inside
routes
folder. - Delete
components/header
folder. - Delete
tests/header.test.tsx
file.
Now, we need to clean up ourapp
component. The following is a cleaned-up version of app.tsx
using class
implementation:
xxxxxxxxxx
import { Component, h } from "preact";
export default class App extends Component {
render() {
return (
<div id="app">
Hello World!
</div>
);
}
}
On package.json
, update the serve
and dev
scripts so that we can access our application at localhost:3001
xxxxxxxxxx
...
"scripts" : {
...
"serve": "sirv build --port 3001 --cors --single",
"dev": "preact watch --port 3001",
...
}
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:
xxxxxxxxxx
npm install reflect-metadata inversify inversify-inject-decorators
npm install -D babel-plugin-transform-typescript-metadata babel-plugin-parameter-decorator
Thereflect-metadata
package allows our decorators on classes and properties to emit their metadata.
inversify
andinversify-inject-decorators
give 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.
xxxxxxxxxx
{
"plugins": [
// https://github.com/inversify/InversifyJS/issues/1004#issuecomment-642118301
// Fixes @inject on constructor
"babel-plugin-transform-typescript-metadata",
"babel-plugin-parameter-decorator"
],
"presets": [
"preact-cli/babel"
]
}
Then we need to tell Preact to use our.babelrc
file for building the App. Open thepreact.config.js
and add the following code as the first line of code on thewebpack(...){
function.
xxxxxxxxxx
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.
xxxxxxxxxx
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:
xxxxxxxxxx
import { Container } from 'inversify';
import getDecorators from 'inversify-inject-decorators';
// Must have https://www.npmjs.com/package/babel-plugin-transform-typescript-metadata
// setup the container...
export const container = new Container({ autoBindInjectable: true });
// Additional function to make property decorators compatible with babel.
let { lazyInject: originalLazyInject } = getDecorators(container);
function fixPropertyDecorator<T extends Function>(decorator: T): T {
return ((args: any[]) => (
target: any,
propertyName: any,
decoratorArgs: any[]
) => {
decorator(args)(target, propertyName, decoratorArgs);
return Object.getOwnPropertyDescriptor(target, propertyName);
}) as any;
}
export const lazyInject = fixPropertyDecorator(originalLazyInject);
Besides initializing the container, we are also overriding thelazyInject
decorator here. Our transform plugin for babel has some issues when@inject
is used with properties. To mitigate that we need to override the defaultlazyInject
as 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
xxxxxxxxxx
import { injectable } from 'inversify';
@injectable()
export default class StorageService {
_inMemoryStorage: { [key: string]: any } = {};
store(key: string, value: any) {
this.getLocalStorage().setItem(key, JSON.stringify(value))
}
getAsJSON(key: string) {
const storedData = this.getLocalStorage().getItem(key);
return storedData ? JSON.parse(storedData) : null;
}
getLocalStorage() {
if (globalThis['localStorage']) {
return localStorage;
}
return this.getInMemoryStorage();
}
getInMemoryStorage() {
return {
setItem: (key: string, value: any) => {
this._inMemoryStorage[key] = value;
},
getItem: (key: string) => {
return this._inMemoryStorage[key];
}
};
}
}
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:
xxxxxxxxxx
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
xxxxxxxxxx
import { inject, injectable } from 'inversify';
import StorageService from './storage.service';
const TODO_LIST_STORAGE_KEY = 'TODO_LIST_STORAGE_KEY';
// Event type to listen for changes to the TODO list
export const LIST_CHANGED_EVENT_TYPE_ID = 'list-changed';
@injectable()
export default class TODOListService {
constructor(
@inject('StorageService') private storageService: StorageService,
) {
}
addListItem(item: string) {
let todoList = this.storageService.getAsJSON(TODO_LIST_STORAGE_KEY);
todoList = todoList || [];
todoList.push(item);
this.storageService.store(TODO_LIST_STORAGE_KEY, todoList);
this.notifyListChanged();
}
getListItems() {
let todoList = this.storageService.getAsJSON(TODO_LIST_STORAGE_KEY);
todoList = todoList || [];
return todoList;
}
getItemCount() {
let todoList = this.storageService.getAsJSON(TODO_LIST_STORAGE_KEY);
todoList = todoList || [];
return todoList.length;
}
// In practice, this should a Subject to which others can subscribe.
// But for the purpose of this article, I will just emit this as an event
notifyListChanged() {
window.dispatchEvent(new Event(LIST_CHANGED_EVENT_TYPE_ID))
}
}
Register this service with the container the same way you added StorageService
xxxxxxxxxx
container.bind('TODOListService').to(TODOListService).inSingletonScope();
Write the TODO List App
Let's update our app.tsx to the following:
xxxxxxxxxx
import { Component, createRef, h } from "preact";
import { lazyInject } from '../di/services.container';
import TODOListService from '../services/todo-list.service';
import { LIST_CHANGED_EVENT_TYPE_ID } from '../services/todo-list.service';
interface AppState {
itemCount: number;
listItems: string[];
}
export default class App extends Component<any, AppState> {
@lazyInject('TODOListService') todoListService: TODOListService;
// Creates a reference to our html input element so that we can get its value
inputElement = createRef<HTMLInputElement>();
constructor() {
super();
this.state = {
itemCount: this.todoListService.getItemCount(),
listItems: this.todoListService.getListItems() || [],
}
// When event an item is added to the todoList, this will be notified of it.
window.addEventListener(LIST_CHANGED_EVENT_TYPE_ID, () => {
this.setState({ itemCount: this.todoListService.getItemCount() });
});
// Update the item list for rendering them on the page
window.addEventListener(LIST_CHANGED_EVENT_TYPE_ID, () => {
this.setState({ listItems: this.todoListService.getListItems() });
});
}
addAndClearItem($event) {
const item = this.inputElement.current.value;
if (item && item.length > 0) {
this.todoListService.addListItem(item);
}
this.inputElement.current.value = "";
}
render() {
return (
<div id="app">
<div class="list-item-count">
You have {this.state.itemCount} items todo.
</div>
<div>
<div>
<input ref={this.inputElement} type="text" />
<button onClick={($event) => this.addAndClearItem($event)}>Add</button>
</div>
</div>
<div class="todo-list">
{
// Render current list
this.state.listItems.map(item => {
return <div>{item}</div>
})
}
</div>
</div>
);
}
}
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.
Opinions expressed by DZone contributors are their own.
Comments