Dependency Injection

Components should focus on rendering. Business logic, API calls, and shared state belong in services. @Service and @Inject wire them together without tight coupling.

Defining a service

@Service
class AuthService {
  @State private token: string | null = null;

  async login(email: string, password: string) {
    const res = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    });
    this.token = (await res.json()).token;
  }

  @Computed
  get isLoggedIn() {
    return this.token !== null;
  }
}

Injecting into components

Register services in @Component and use @Inject to access them:

@Component({
  services: [AuthService]
})
class App extends BaseComponent {
  @Inject(AuthService) auth!: AuthService;

  view() {
    return <div>Logged in: {this.auth.isLoggedIn}</div>;
  }
}

Every descendant of App can also inject AuthService — no need to re-register it.

Service-to-service dependencies

Services can depend on other services:

@Service
class UserRepository {
  @Inject(AuthService) auth!: AuthService;

  async fetchProfile() {
    if (!this.auth.isLoggedIn) throw new Error('Not authenticated');
    // ...
  }
}

Scoping

Services registered in a component are available to that component and all its descendants. This creates natural boundaries:

@Component({ services: [AuthService, UserRepository] })
class App extends BaseComponent { ... }
  // AuthService available here and everywhere below

  @Component({ services: [CartService] })
  class ShopPage extends BaseComponent { ... }
    // CartService available here and below
    // AuthService also available (inherited from App)

  @Component()
  class ProfilePage extends BaseComponent { ... }
    // AuthService available (inherited from App)
    // CartService NOT available (different subtree)

Lifecycle hooks

@Service
class WebSocketService {
  private socket!: WebSocket;

  async onBootstrap() {
    // runs after instantiation and injection
    this.socket = new WebSocket('wss://api.example.com');
  }

  onDestroy() {
    // runs when the owning component disconnects
    this.socket.close();
  }
}