← Retour au blog
Angulardependency-injectioninject()modern-patternssignals

Beyond inject(): When to Actually Abandon the Angular Constructor in 2025

Angular 14 introduced the inject() function as an alternative to the classic constructor for dependency injection. Since then, teams have oscillated between nostalgia for the constructor and adoption of the new paradigm. The truth is more nuanced: inject() is not universally superior, and its strategic adoption depends on the component or service context. Understanding the differences in timing, readability, and compatibility with new reactive primitives becomes essential for writing maintainable code.

Execution Timing: An Invisible but Critical Difference

The constructor executes before the component enters its initialization phase. At that point, parent directives have not yet been applied, and values injected via custom configuration tokens are not guaranteed to be available. inject(), on the other hand, executes in the current injection context—often during the component or service creation method. This means providers defined in the component itself, or inherited from parents, are visible. Concretely, if you inject a service via a token provided by a parent component, the constructor will fail silently or throw an error, while inject() will find the right provider. This difference becomes glaring in advanced patterns like dynamically injected tokens or component-level configurations.

const MyService = inject(ConfigService); // Sees providers from parent component

@Component({ providers: [{ provide: ConfigService, useValue: customConfig }] }) export class MyComponent { constructor(private config: ConfigService) {} // May not see the custom provider }

Signals and Reactivity: inject() Wins Naturally

With the arrival of effect(), computed(), and signals, inject() integrates more smoothly. A signal injected via inject() inside an effect() or computed() automatically creates a reactive dependency—Angular tracks calls to .value() and updates the computation. With the constructor, you must manually manage RxJS subscriptions or ChangeDetectionStrategy.OnPush. Take a simple example: you inject a theme signal and want to update a CSS class. With inject() and effect(), it's one line. With the constructor and RxJS, you must create a Subscription, destroy it in OnDestroy, and manage change detection. It's not insurmountable, but it's more verbose. New teams starting with signals will find inject() more coherent with their mental model.

export class ThemeComponent { private themeSignal = inject(ThemeService).currentTheme;

constructor(private renderer: Renderer2) { effect(() => { const theme = this.themeSignal(); this.renderer.addClass(document.body, theme-${theme} ); }); } }

Readability and Introspection: The Debate Persists

The constructor remains easier to read for classic Angular developers. Dependencies are visible at a glance: the constructor's typed parameters serve as a readable contract. inject() scatters declarations throughout the component or service body, which can seem less structured at first. However, inject() offers superior flexibility: you can inject conditionally, with defaults, or only in branches that use them. With the constructor, you inject everything, even what's unnecessary. For services with 5-7 dependencies, the constructor becomes unwieldy. For simple components with 2-3 dependencies, the constructor remains more readable. Automated introspection (refactoring tools, linters) works better with the constructor because the syntax is standardized. inject() requires more sophisticated heuristics.

Testability: inject() Complicates Slightly

In tests, injecting mocks via the constructor is trivial: you pass dependencies to new. With inject(), you must configure TestBed and provide mocks via providers. This is not a blocker—it's been the standard pattern since Angular 2—but it means tests for services with inject() always require a TestBed, whereas tests for services with a constructor can use plain instances. For components, the difference fades: both approaches require a TestBed. But for utility functions or pure services you test in isolation, the constructor remains lighter. If you write many fast unit tests, this matters.

The Common Pitfall: Mixing Both Approaches

The biggest mistake teams make is mixing inject() and the constructor haphazardly. One service uses inject(), another uses the constructor, and suddenly coherence breaks. Newcomers wonder when to use what. Worse, if you inherit a class (rare but possible), dependencies from the parent injected via inject() are not accessible to the child class—you must redeclare them or pass them via the constructor. This confusion generates subtle bugs and makes code hard to maintain. The rule must be clear at the team level: either you adopt inject() everywhere (modern, coherent with signals), or you stick with the constructor (compatible, readable). A middle ground—using inject() for signals and the constructor for RxJS services—creates more friction than it resolves.

When to Choose inject(): The Practical Matrix

Use inject() if your project is new or undergoing refactoring, you target Angular 17+, and you're adopting signals. Use it also if you need conditional injection, optional dependencies (with inject(Service, { optional: true })), or if you're injecting custom tokens provided by parent components. For simple infrastructure services (HTTP, Logger, Config), inject() adds little benefit and the constructor remains more readable. If your team includes developers less familiar with modern patterns, the constructor is a solid starting point. For libraries you maintain that must support Angular 12-18, stick with the constructor: inject() requires Angular 14+, and users of earlier versions will be blocked.

Conclusion: Pragmatism Over Dogma

There is no universal answer. inject() is more modern, more flexible, and integrates better with signals. But the constructor remains valid, more readable in certain contexts, and easier to test in isolation. The right approach is to make a decision at the team level and document it. Create a simple ESLint rule (@angular-eslint) to enforce it, and avoid ad-hoc debates at every pull request. If you move to inject(), start with business services and components that manipulate signals. Infrastructure services and simple presentational components can wait. Above all, don't let modernity for modernity's sake dictate an unnecessary refactoring of a codebase that already works well. Angular has this strength: it lets you choose, and it's your responsibility to do so wisely.

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️