Skip to main content

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.