Dependency Injection
Resolving Services
ServiceProvider.get and getAsync, getService in a component, FromService, inject, deps and useApplication.
Resolving a service
A registered service is asked for from the provider. In a component you use getService or FromService, while a service is being built inject or deps, and anywhere else the provider itself (app.provider).
ServiceProvider.get
const provider = app.provider;
const notes = provider.get(NotesService);
const provider = app.provider;
const notes = provider.get(NotesService);
The router resolves a route component from the provider if it is registered there (add* or @Injectable), and builds it directly if it is not. If a registered component cannot be resolved (a missing dependency, a cycle, a constructor error), the navigation fails with MJX304, the original error is carried in cause, and fallbacks.error is shown.
get is synchronous and throws an error (MJX402) for a service registered with an async factory or a Promise; resolve those with await provider.getAsync(token). provider.createScope(name?) produces a new child scope; scoped services have a single instance per scope.
From a place that has no access to the application instance (a helper module, a route hook), the running application's provider is reached through Application.main.provider:
const notes = Application.main.provider.get(NotesService);
const notes = Application.main.provider.get(NotesService);
getService from a component
getService finds the nearest provider and resolves the token; if none is found or the resolution fails, it returns null (in development mode an MJX407/MJX408 warning is written). In the example, the header of the notes list writes the total number of notes to its title attribute while it is being built:
const notes = this.getService(NotesService);
if (notes) this.attr.add({ title: `${notes.count("notes")} notes` });
const notes = head.getService(NotesService);
if (notes) head.attr.add({ title: `${notes.count("notes")} notes` });
const notes = this.getService(NotesService);
if (notes) this.attr.add({ title: `${notes.count("notes")} notes` });
const notes = head.getService(NotesService);
if (notes) head.attr.add({ title: `${notes.count("notes")} notes` });
In class and Options components getService is on the component itself; these lines are written with this in a hook (onConfig, onConfigured) or in a field initializer.
In a function component getService is on the component itself (head in the example): in JSX, the component that reaches the root's onConfig hook or ref, and without JSX, the component received by the function of the motifComponent call that builds the root. When the component is not at hand, FromService does the same job.
If the service is not registered, getService and FromService return null; inject, provider.get and provider.getAsync throw when they cannot find it.
When a class token is given, the return type is that class; this includes classes whose constructor takes parameters, abstract classes and classes with a protected constructor. getService and FromService give a nullable type (T | null), while inject, provider.get and provider.getAsync give a non-null type. Writing the generic explicitly (getService<NotesService>(NotesService)) gives the same type; it can be used to type the class through an interface: FromService<NoteStore>(NotesService). For a string or symbol token, the type is given with the generic: getService<{ apiUrl: string }>("config").
The FromService helper
For quick resolution anywhere (outside components as well):
import { FromService } from "@motifx/core";
const notes = FromService(NotesService); // null if it cannot be resolved
import { FromService } from "@motifx/core";
const notes = FromService(NotesService); // null if it cannot be resolved
If the service is not registered, there is a circular dependency, or the constructor throws, it returns null; in development mode an MJX414 warning is written together with the original error. For a scoped service it gives the handle that leads to the current navigation's instance.
inject(token) — taking a dependency
inject is called in a class's field initializers and constructor while the provider is building that class, or inside a useFactory factory while the provider is calling it. It resolves the dependency from the provider that builds the class: a scoped service is the instance of the route scope that asks for it. The return type comes from the token.
@Injectable()
export class SearchIndex {
private notes = inject(NotesService);
private config = inject<{ apiUrl: string }>("config");
private readonly size: number;
constructor() {
this.size = this.notes.count("notes"); // can be used in the constructor too
}
}
@Injectable()
export class SearchIndex {
notes = inject(NotesService);
config = inject("config");
size;
constructor() {
this.size = this.notes.count("notes"); // can be used in the constructor too
}
}
inject works only at setup time. Inside a method, in a setTimeout callback, or when the class is built by hand with new SearchIndex(), it throws an MJX409 error. A token that is not registered gives MJX401, and a circular dependency MJX404. inject is synchronous; take async registrations that need getAsync with deps or getAsync.
In class and Options components, use this.getService(token) for a dependency.
In a function component, use FromService(token) for a dependency.
deps — constructor parameters
Dependencies can also be given to the constructor as parameters. The tokens in the deps array are resolved in order and passed to the constructor in the same order:
@Injectable({ deps: [NotesService, "config"] })
export class SearchIndex {
constructor(private notes: NotesService, private config: { apiUrl: string }) {}
}
@Injectable({ deps: [NotesService, "config"] })
export class SearchIndex {
constructor(notes, config) {
this.notes = notes;
this.config = config;
}
}
TypeScript does not check the order; deps and the constructor parameters have to be written in the same order. In this form the class can be built by hand in a test with fake dependencies: new SearchIndex(fakeNotes, { apiUrl: "/test" }).
The useApplication hook
useApplication() gives the running application, its provider and the router in a single object; attach(...components) adds components to the application shell:
import { useApplication } from "@motifx/core";
const { application, services, router, attach } = useApplication();
const notes = services.get(NotesService);
import { useApplication } from "@motifx/core";
const { application, services, router, attach } = useApplication();
const notes = services.get(NotesService);
Next step
To learn when and how services are disposed, go on to Disposing Services.