FrançaisPlayground

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.