Dependency injection
@fluixi/start/di provides an Angular-style injector — useful for sharing services,
clients and config without prop-drilling, with per-request isolation on the server.
import { injectionToken, provide, inject } from '@fluixi/start/di';
const ApiClientToken = injectionToken<ApiClient>('ApiClient');
// Provide it, e.g. at the app root:
provide([
{ provide: ApiClientToken, useFactory: () => new ApiClient(env.API_URL) },
]);
// Inject it anywhere in the owner tree:
const api = inject(ApiClientToken);
provide takes an array of providers, so one call registers a whole set, and returns
the injector it created.
Tokens
A token is an identity, not a value — two tokens with the same name are still distinct,
and the type parameter is what makes inject return the right type.
const Config = injectionToken<AppConfig>('Config');
const Now = injectionToken<() => Date>('Now', { factory: () => () => new Date() });
A token with a factory has a default, so inject(Now) works with nothing provided —
useful for things that almost always have one sensible implementation and occasionally
need swapping in a test.
Provider forms
provide([
{ provide: Config, useValue: { apiUrl: '/api' } }, // a ready value
{ provide: Clock, useClass: SystemClock }, // constructed once
{ provide: Api, useFactory: (c) => new Api(c.apiUrl), deps: [Config] },
{ provide: LegacyApi, useExisting: Api }, // an alias
]);
deps lists the tokens to resolve and pass, in order, to useFactory or the useClass
constructor. Everything is built lazily on first inject and then cached, so a
service nobody injects is never constructed.
A circular dependency is detected and reported rather than overflowing the stack.
Scope
Injectors follow the reactive owner tree, so a provide inside a component shadows an
outer one for that subtree and disappears with it:
createRoot(() => {
provide([{ provide: T, useValue: 'child' }]);
inject(T); // "child"
});
inject(T); // back to the outer value
provideRoot registers on the root injector instead, which is what an app-wide service
usually wants. runInInjectionContext(injector, fn) runs fn against a specific
injector — needed when you have to inject from a callback that has left the owner tree.
Per-request roots
On the server the root injector is created per request, so two concurrent requests never share instances. A request-scoped service — a database connection, the current user's session — stays isolated without any work on your part. On the client there is one root for the life of the page.
This is the reason to prefer DI over a module-level singleton in an SSR app: a module global is shared by every request the process handles, and leaking one user's session into another's render is the kind of bug that only appears under concurrency.
Cleanup
A provider can implement onDestroy, called when its injector is disposed — at the end of
the request on the server, or when the owning root is disposed on the client.
Next: Internationalization.