Dependency injection

A hierarchical injector resolves services by scope: each module and each mounted view gets its own injector, parents delegate upward, and where a provider is registered decides who shares the instance.

Providers

Four provider forms, same as Angular:

@Module({
  providers: [
    TodoStore,                                   // class → singleton per scope
    { provide: API_URL, useValue: '/api/v2' },   // value
    { provide: Logger, useClass: HttpLogger },   // alias another class
    { provide: QueryClient, useFactory: () => new QueryClient() }, // factory
    { provide: Clock, useFactory: (api: Api) => new Clock(api), deps: [Api] },
  ],
})
class AppModule {}

InjectionToken<T> gives non-class dependencies a typed, rename-safe key:

export const API_URL = new InjectionToken<string>('API_URL');

// consumer side
private apiUrl = inject(API_URL);

inject() — construction-time only

inject() reads from the injector that is currently constructing the instance. It belongs in field initializers or the constructor — never in render(), callbacks, or async work, where it throws No active injection context:

@Component()
export class TodoView extends View {
  private store = inject(TodoStore);      // field initializer — correct
  private c = inject(TodoController);     // resolved in this view's scope

  render() {
    // inject(Anything) here would throw — render is not a context
    return <p>{this.store.todos.value.length}</p>;
  }
}

For code outside construction — a factory, an integration helper — wrap the call in runInInjectionContext(injector, fn), or read the injector with getCurrentInjector() during construction and call injector.get(token) later.

Scoping — where a provider lives decides its lifetime

Registered onInstance lifetime
providedIn: 'root' on @InjectableOne for the whole app, resolved from the root injector
@Module({ providers })One per module injector — shared by every route, view, and controller under that module
@Component({ providers })One per mounted view instance — the view's own Angular-style component providers
Declared controllers (module or component)Shared down that injector's subtree
Undeclared @Controller / injectableTransient — a fresh instance per injecting scope

Resolution walks the chain view → route module → app → root. A closer registration shadows a farther one — an Angular-style override at any level.

Lifecycle and cleanup

Every injector owns a DestroyRef. Inject it during construction and register teardown — it fires when the scope deactivates (view unmounts, module injector is released):

@Injectable()
export class ThemeService {
  theme = signal<Theme>(this.readStored());

  constructor() {
    const mq = matchMedia('(prefers-color-scheme: dark)');
    const onChange = () => this.theme.value = 'system';
    mq.addEventListener('change', onChange);
    inject(DestroyRef).onDestroy(() =>
      mq.removeEventListener('change', onChange));
  }
}

Views and controllers implement onInit()/onDestroy() — called when their scope activates and deactivates, guaranteed paired even on same-tick mount/unmount. See Views & lifecycle.