Dependency Injection
Introduction to Dependency Injection
Registering services and resolving them from components; a full example from registration to use, and the DI error codes; with and without JSX.
MotifJS includes a built-in dependency injection (DI) system. You register services when the application starts, and then resolve them from components or from other services. A service is a class or a value that holds state and work shared between components. A component does not build the service itself; it asks the provider (ServiceProvider) for it, and the provider gives either the same instance or a new one, depending on the registration's lifetime.
From registration to use
Services are registered on the services collection of Application.CreateBuilder(), before build(). addSingleton(token, { useClass }) registers the class with a single instance; a singleton given startup: true is created at the start of app.run() without a component asking for it. The two registrations in the example (NotesService and SettingsService from the Components page):
import { Application } from "@motifx/core";
import { NotesService } from "./services/notes";
import { SettingsService } from "./services/settings";
const builder = Application.CreateBuilder();
builder.services.addSingleton(NotesService, { useClass: NotesService });
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
import { Application } from "@motifx/core";
import { NotesService } from "./services/notes";
import { SettingsService } from "./services/settings";
const builder = Application.CreateBuilder();
builder.services.addSingleton(NotesService, { useClass: NotesService });
builder.services.addSingleton(SettingsService, { useClass: SettingsService, startup: true });
const app = builder.build();
A component gets a registered service from the provider: a class component and an Options object use this.getService(token), and a function component uses FromService(token). When the service's reactive state is read inside a getter, the binding refreshes as the state changes. FolderBadge in the example shows the number of notes in a folder:
import { Component } from "@motifx/core";
import { NotesService, type Folder } from "../services/notes";
export class FolderBadge extends Component<HTMLSpanElement, { folder: Folder }> {
notes = this.getService(NotesService)!;
override onConfigured() {
this.class.add("badge");
}
override view() {
return <>{() => this.notes.count(this.props.folder)}</>;
}
}
import { FromService } from "@motifx/core";
import { NotesService, type Folder } from "../services/notes";
export function FolderBadge(props: { folder: Folder }) {
const notes = FromService(NotesService)!;
return <span class="badge">{() => notes.count(props.folder)}</span>;
}
import { motifOptions } from "@motifx/core";
import { NotesService, type Folder } from "../services/notes";
export const FolderBadge = (props: { folder: Folder }) => motifOptions({
el: "span",
get notes() {
return this.getService(NotesService)!;
},
onConfigured() {
this.class.add("badge");
},
view() {
return <>{() => this.notes.count(props.folder)}</>;
},
});
import { Component, ComponentBase, motifComponent } from "@motifx/core";
import { NotesService, type Folder } from "../services/notes";
export class FolderBadge extends Component<HTMLSpanElement, { folder: Folder }> {
static override elementTag = "span";
notes = this.getService(NotesService)!;
override onConfigured() {
this.class.add("badge");
}
override view() {
return motifComponent("text", (node: ComponentBase) => node.bindings.text(() => this.notes.count(this.props.folder)));
}
}
import { ComponentBase, FromService, motifComponent } from "@motifx/core";
import { NotesService, type Folder } from "../services/notes";
export function FolderBadge(props: { folder: Folder }) {
const notes = FromService(NotesService)!;
return motifComponent("span", (badge: ComponentBase) => {
badge.class.add("badge");
badge.controls.add(motifComponent("text", (node: ComponentBase) => node.bindings.text(() => notes.count(props.folder))));
});
}
import { ComponentBase, motifComponent, motifOptions } from "@motifx/core";
import { NotesService, type Folder } from "../services/notes";
export const FolderBadge = (props: { folder: Folder }) => motifOptions({
el: "span",
get notes() {
return this.getService(NotesService)!;
},
onConfigured() {
this.class.add("badge");
},
view() {
return motifComponent("text", (node: ComponentBase) => node.bindings.text(() => this.notes.count(props.folder)));
},
});
import { Component } from "@motifx/core";
import { NotesService } from "../services/notes";
export class FolderBadge extends Component {
static elementTag = "span";
notes = this.getService(NotesService);
onConfigured() {
this.class.add("badge");
}
view() {
return <>{() => this.notes.count(this.props.folder)}</>;
}
}
import { FromService } from "@motifx/core";
import { NotesService } from "../services/notes";
export function FolderBadge(props) {
const notes = FromService(NotesService);
return <span class="badge">{() => notes.count(props.folder)}</span>;
}
import { motifOptions } from "@motifx/core";
import { NotesService } from "../services/notes";
export const FolderBadge = (props) => motifOptions({
el: "span",
get notes() {
return this.getService(NotesService);
},
onConfigured() {
this.class.add("badge");
},
view() {
return <>{() => this.notes.count(props.folder)}</>;
},
});
import { Component, motifComponent } from "@motifx/core";
import { NotesService } from "../services/notes";
export class FolderBadge extends Component {
static elementTag = "span";
notes = this.getService(NotesService);
onConfigured() {
this.class.add("badge");
}
view() {
return motifComponent("text", (node) => node.bindings.text(() => this.notes.count(this.props.folder)));
}
}
import { FromService, motifComponent } from "@motifx/core";
import { NotesService } from "../services/notes";
export function FolderBadge(props) {
const notes = FromService(NotesService);
return motifComponent("span", (badge) => {
badge.class.add("badge");
badge.controls.add(motifComponent("text", (node) => node.bindings.text(() => notes.count(props.folder))));
});
}
import { motifComponent, motifOptions } from "@motifx/core";
import { NotesService } from "../services/notes";
export const FolderBadge = (props) => motifOptions({
el: "span",
get notes() {
return this.getService(NotesService);
},
onConfigured() {
this.class.add("badge");
},
view() {
return motifComponent("text", (node) => node.bindings.text(() => this.notes.count(props.folder)));
},
});
The component is used with its tag:
<RouterLink to="/notes" activeClass="active">All notes <FolderBadge folder="notes" /></RouterLink>
const all = motifComponent(RouterLink, {
to: "/notes",
activeClass: "active",
childs: [motifComponent("text", (node: ComponentBase) => node.setText("All notes ")), motifComponent(FolderBadge, { folder: "notes" })],
});
<RouterLink to="/notes" activeClass="active">All notes <FolderBadge folder="notes" /></RouterLink>
const all = motifComponent(RouterLink, {
to: "/notes",
activeClass: "active",
childs: [motifComponent("text", (node) => node.setText("All notes ")), motifComponent(FolderBadge, { folder: "notes" })],
});
Every component that asks for a service registered as a singleton gets the same instance. If the service keeps its state in a reactive object, a change made by one component (for example notes.create()) refreshes every binding that reads that state; no separate event or notification is needed.
A function component has no this, so FromService takes the service from the application's provider; for a scoped service, this also gives the handle of the current navigation. The details are on the Resolving Services page, the other registration forms and automatic registration with @Injectable on the Registering Services page, and how long a service lives on the Lifetimes and Scopes page.
DI error codes
Thrown codes arrive as an error on the line that made the call; warnings are written only in development mode, to the console and to the DevTools warning list, and do not go to the errorHandler listeners (see Error Handling). All the codes are on the Error and Warning Codes page:
| Code | When |
|---|---|
MJX401 |
A token that is not registered was asked for with get, getAsync or inject; thrown. |
MJX402 |
A service registered with an async factory or a Promise was asked for with get (a startup service included); thrown, use getAsync. |
MJX403 |
The registration definition is invalid; thrown. |
MJX404 |
A circular dependency; thrown. |
MJX405 |
A second ApplicationBuilder was set up; thrown. |
MJX406 |
app.run could not find the host element given as a selector; thrown. |
MJX407 |
getService could not find a provider for the component; a warning in development mode, returns null. |
MJX408 |
The getService resolution failed; a warning in development mode, returns null. |
MJX409 |
inject was called outside the setup of a service; thrown. |
MJX410 |
Several sources were given in one registration; a warning in development mode, the precedence is useValue > useFactory > useClass. |
MJX411 |
deps is not an array; a warning in development mode, the invalid deps is ignored. |
MJX413 |
A function that cannot be constructed (an arrow function, an async function, an object method) was registered as a class; thrown on the registration line. |
MJX414 |
FromService could not resolve; a warning in development mode, returns null. |
MJX415 |
startup was given to a registration that is not a singleton; a warning in development mode, the service is created on its first resolution. |
Next step
To learn how services are registered, go on to Registering Services.