Dependency Injection
HexaJS uses a lightweight decorator-based DI model with context-aware validation during AOT build.
Context-Aware DI and AOT
DI is not only runtime resolution. During build, HexaJS scans decorators and validates dependencies against runtime context boundaries:
- Background classes can inject background or general services.
- Content classes can inject content or general services.
- UI classes can inject UI or general services.
- Cross-context violations fail at build time.
This context-awareness is extracted from @Injectable({ context: InjectableContext.* }) and enforced before bootstrap generation.
Context Boundaries and Lifecycle
Each runtime context has an isolated DI container and lifecycle:
- Background: service-worker lifecycle, owns controllers and long-lived orchestration.
- Content: per-page lifecycle, owns handlers and DOM integration.
- UI: popup/devtools lifecycle, messaging-focused DI with
HexaUIClient.
Because each context runs in a separate runtime world, HexaJS does not allow cross-context service injection.
Practical lifecycle guidance
- Create subscriptions in
onInit(). - Dispose subscriptions and listeners in
onDestroy(). - Keep state ownership in Background/Content stores, not in Managed UI.
This lifecycle model is generated into per-context bootstrap code by the CLI.
Generate a Service First
Use the CLI to scaffold a service in the correct context:
hexa generate service logger background
hexa generate service page-dom content
hexa generate service app-config general
hexa generate service ui-state ui
Valid service contexts are background, content, general, and ui.
Declaring a service
import { Injectable, InjectableContext } from '@hexajs-dev/common';
@Injectable({ context: InjectableContext.Background })
export class TabQueryService {
getActiveIdFromTabs(tabs: Array<{ id?: number }>): number {
return tabs[0]?.id ?? -1;
}
}
Injecting Services in Background and Content
The same injection pattern works in both contexts, but each class can only consume services valid for its context.
import { Inject } from '@hexajs-dev/common';
import { Controller, Action } from '@hexajs-dev/core';
import { TabsPort } from '@hexajs-dev/ports';
import { TabQueryService } from '../services/tab-query.service';
@Controller({ namespace: 'tabs' })
export class TabsController {
constructor(private tabsPort: TabsPort, private tabsService: TabQueryService) {}
@Action('active-id')
async activeId(): Promise<{ tabId: number }> {
const tabs = await this.tabsPort.queryTabs({ active: true, currentWindow: true });
return { tabId: this.tabsService.getActiveIdFromTabs(tabs) };
}
}
import { Handler, Handle } from '@hexajs-dev/core';
import { LoggerService } from '../services/logger.service';
import { MyContentEntry } from './content';
@Handler({ namespace: 'tabs', Contents: [MyContentEntry] })
export class TabsHandler {
constructor(private logger: LoggerService) {}
@Handle('active-id')
onActiveId(payload: { tabId: number }): { ok: boolean } {
this.logger.log('Active tab id from background:', payload.tabId);
return { ok: true };
}
}
Tokens and Injection
For values (not classes), use token injection with @Inject(...):
import { Inject, Injectable, HEXA_PLATFORM } from '@hexajs-dev/common';
@Injectable()
export class PlatformLabelService {
constructor(@Inject(HEXA_PLATFORM) private platform: string) {}
getLabel(): string {
return `running on ${this.platform}`;
}
}
Use constructor injection and decorators in userland code. Container setup and token registration are generated by the AOT bootstrap.
Store-aware DI usage
Store classes are injected by context:
HexaBackgroundStore<T>for background classes.HexaContentStore<T>for content classes.
See State Management for complete action/reducer/select patterns.