NgClass vs class attribute
When working with Angular templates, you have the option to conditionally apply CSS classes using either the `ngClass` directive or the `class` attribute. Each comes with its own set of strengths.NgClass
Upsides: While both `[class]` and `NgClass` can apply multiple classes to an element:<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">
...
</div>
`NgClass` goes further by supporting keys that contain spaces:
<div [ngClass]="{'one two': isActive}">
...
</div>
The same approach won't work with `[class]`, and in that case, neither of the classes would be applied.
Downsides:
When you only need to toggle a single dynamic class, `NgClass` can feel heavier than the `class` attribute. Additionally, it's a directive, which means it must be imported into the component. On top of that, `NgClass` can also bind to a JavaScript `Set` of strings to represent classes, a feature that's uncommon but can be handy in specific scenarios.
Class attribute
Upsides: The `class` attribute is a native HTML attribute, so you're using Angular's binding mechanism without needing to import any directive. It also tends to be more concise when applying just one class. Consider this example:<div [class.disabled]="isDisabled">
...
</div>
And then compare it to this:
<div [ngClass]="{'disabled': isDisabled}">
...
</div>
As shown, the `class` binding is slightly shorter.
Downsides:
The `class` attribute can either be bound to a single class or to an object with boolean values, provided the keys don't contain spaces. Unlike `ngClass`, it doesn't support other data formats.
How to choose?
In this case, there are really only two sensible strategies:
- Pick one method and apply it consistently across all scenarios
- Use `class` binding for straightforward cases and reserve `ngClass` for more complex ones or those involving data structures like `Set`
"inject" function vs Constructor DI
Since Angular v14, developers have had the option to use the `inject` function for dependency injection instead of going through the constructor. Many have already made the switch, and Angular has transitioned several of its own building blocks, such as interceptors and guards, to a functional style that relies on `inject`. Even so, the debate lingers, with some developers hesitant to change their ways. Let's sort out how to proceed."inject" function
Upsides: The `inject` function is more succinct, eliminates the need for a constructor entirely, and can be used in any injection context (meaning it must be called from a constructor or a DI factory function), not just within classes. It also simplifies applying DI lookup options. Take this example:@Component({...})
export class MyComponent {
constructor(
@Optional() @SkipSelf() private readonly someService: SomeService,
) {}
}
And compare it to this:
@Component({...})
export class MyComponent {
private readonly someService = inject(SomeService, {optional: true, skipSelf: true});
}
The `inject` function also streamlines handling typings for `InjectionToken`-s. Here's the difference:
export class MyService {
constructor(
@Inject(SOME_TOKEN) private readonly token: SomeTokenType,
) {}
}
VS
export class MyService {
private readonly token = inject(SOME_TOKEN); // this will implicitly have the type SomeTokenType
}
It's worth highlighting that the earlier example, using the `@Inject` decorator, depends on an experimental TypeScript feature called `experimentalDecorators`, which may be phased out as the ECMAScript decorators specification evolves differently from what TypeScript currently supports (constructor argument decorators aren't part of the proposal). For more details, see the discussion here. This consideration tilts the scale toward `inject`.
Downsides:
The `inject` function can occasionally lead to confusion when it's used outside of an injection context. You can call it within any function, but that function ultimately needs to be invoked from a component or service constructor. It's important to clarify that this isn't a real limitation, since constructor DI requires a constructor anyway, so in practice, we've actually broadened what's possible with dependency injection. Any error that surfaces is typically straightforward to diagnose and resolve.
A more tangible drawback appears when unit testing services. With constructor DI, you can easily instantiate a service and pass mock dependencies directly through the constructor. Here's what that looks like:
describe('SomeService' () => {
let instance: SomeService;
beforeEach(() => {
instance = new SomeService(otherServiceMock);
});
});
This is straightforward for classes relying on constructor DI. With the `inject` function, however, you must run tests within an injection context, which adds verbosity:
describe('SomeService' () => {
let instance: SomeService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{provide: OtherService, useValue: otherServiceMock},
],
});
TestBed.runInInjectionContext(() => {
instance = inject(SomeService);
});
});
});
That's clearly a step back. Still, two important points need to be made.
- This issue is specific to services in unit tests. If you skip unit tests, you won't face it. And when testing components or directives, which must be initialized with `TestBed`, it won't be a problem even if they use the `inject` function.
- Using `TestBed` for testing all Angular building blocks is the recommended path, since `TestBed` mirrors the framework's behavior as closely as possible.
Constructor DI
Upsides: As we've seen, constructor DI is more convenient for unit testing services. It also feels more familiar to developers who've encountered dependency injection in other frameworks or languages, such as .NET. Beyond that, though, the benefits start to taper off. Downsides: As the examples make clear, constructor DI is less adaptable than the `inject` function. It requires a constructor and falls short when dealing with typings for `InjectionToken`-s. How to choose? Once more, we're looking at two main paths:- Adopt the `inject` function across the board. Its only downside isn't significant enough to warrant avoiding it.
- Use `inject` everywhere except in services, to keep unit testing simpler. This is a viable approach, but it can add some complexity.
Single file components vs separate templates
Angular components give you the choice of embedding an inline string as the template or referencing an external HTML file. Since the framework's early days, this has been a frequent point of debate, so let's break down the trade-offs.Separate templates
Upsides: Keeping the HTML in its own file separates it from the component logic, making things feel less opaque. Some IDEs also offer better tooling for this setup compared to inline templates, although the Angular Language Service typically bridges that gap. Downsides: Separate templates might give developers a misleading sense of separation, leading them to think they're organizing concerns properly while they keep piling more code into the templates. They can also be more cumbersome to navigate, as IDEs sometimes struggle when switching between files.Note: If you're leaning toward separate templates, consider checking out this extension, which adds hotkeys for quickly toggling between a component's template, styles, and tests.
Single file components
Upsides: With SFCs, you'll end up with fewer files and folders in your project structure. They can also simplify navigation, since everything related to a component lives in one spot. Downsides: Some view SFCs as a breach of the Single Responsibility Principle, since they merge three different languages—HTML, TypeScript, and CSS—into a single file. This is, of course, open to interpretation; SFC proponents argue that "concerns" should refer to business-level units like features or pages, not the specific technologies involved. In the end, a component's HTML, TS, and CSS files are usually edited together anyway. How to choose? This is largely a matter of personal preference and team dynamics. Still, we can set some guiding principles for either approach to ensure it works well:- Opt for SFCs and impose a limit on the size of any single component file. This is easier to manage with one file, and it encourages keeping components short and focused.
- Choose separate templates and rely on static analysis tools to enforce file length. Here, the key worry is that TS, CSS, and HTML files might each look short individually, but as a combined unit, they could still hold too much logic and accidentally violate the Single Responsibility Principle. That can be countered by adopting a policy that keeps *all* files brief.
Choosing Between Template-Driven and Reactive Forms
Angular provides two main approaches to handling forms: binding with ngModel or leveraging Reactive Forms via FormControl instances. Which one you pick often boils down to taste, but it is worth weighing what each has to offer.
Template-Driven Forms
Advantages:
The barrier to entry is lower here. The ngModel directive attaches directly to a component property, which makes the setup feel native. This gets even easier with signals on board:
@Component({
template: `
<input type="text" [(ngModel)]="text">
{{ text() }}
`,
imports: [FormsModule],
})
export class MyComponent {
text = input<string>();
}
The snippet above shows that a simple import of FormsModule plus the ngModel directive is enough. From there, the signal (or any other bound property) can drive the business logic. The template itself narrates the form's behavior — hence the name.
Drawbacks:
When dealing with signals, pulling the full form value out can get awkward. Take this case:
@Component({
template: `
<input type="text" [(ngModel)]="form.email">
<input type="password" [(ngModel)]="form.password">
`,
imports: [FormsModule],
})
export class MyComponent {
form = {
email: signal(''),
password: signal(''),
};
submitForm() {
// this can become verbose if we have a lot of fields
const formValue = {
email: this.form.email(),
password: this.form.password(),
};
// submit the form
}
}
With Reactive Forms, reading the current state is a matter of accessing form.value.
Validation is another sore spot. Template-driven forms lean on native HTML attributes, while custom rules require dedicated directives, which adds boilerplate.
@Directive({
selector: '[appPositiveNumber]',
providers: [{provide: NG_VALIDATORS, useExisting: PositiveNumbersValidatorDirective, multi: true}],
})
export class PositiveNumberValidatorDirective implements Validator {
validate(control: AbstractControl): ValidationErrors | null {
return (+control.value) > 0 ? null : {positiveNumber: true};
}
}
@Component({
template: `
<input type="number" appPositiveNumber [(ngModel)]="number">
`,
imports: [FormsModule, PositiveNumberValidatorDirective],
})
export class MyComponent {
number = signal(0);
}
In contrast, a reactive validator is just a function:
function positiveNumberValidator(control: AbstractControl): ValidationErrors | null {
return (+control.value) > 0 ? null : {positiveNumber: true};
}
@Component({
template: `
<input type="number" [formControl]="number">
`,
imports: [ReactiveFormsModule],
})
export class MyComponent {
number = new FormControl('', {validators: [positiveNumberValidator]});
}
Reactive Forms
Advantages:
Reactive Forms lay everything on the table: initial values, validators, disabled states, and more are all declared in one place. The template only needs to bind controls to fields, and the framework takes care of the rest.
The form.value property gives you the whole value at once. You also get valueChanges and statusChanges observables out of the box, which makes reacting to form updates straightforward (though signals have closed that gap for template-driven forms).
On a broader level, embracing RxJS nudges developers toward declarative patterns. Instead of chasing state around the component, you describe the flow, which pays off when adopting state management layers like NgRx.
Drawbacks:
Verbosity is the obvious trade-off. Even though the structure is clear, it’s still a wrapper around a value, so you might end up doing extra bookkeeping to keep things in sync. Consider a form where picking a topic should reveal a subtopic dropdown. A template-driven approach, powered by signals, handles this with relative ease:
@Component({
template: `
<form>
<div>
<label for="title">Title</label>
<input type="text" id="title" name="title" [(ngModel)]="form().title" />
</div>
<div>
<label for="topic">Topic</label>
<select id="topic" name="topic" [(ngModel)]="form().topic">
@for (topic of topics(); track topic.id) {
<option [value]="topic.id">{{ topic.name }}</option>
}
</select>
</div>
@if (form().subtopic) {
<div>
<label for="subtopic">Sub Topic</label>
<select id="subtopic" name="subtopic" [(ngModel)]="form().subtopic">
@for (subTopic of subTopics(); track subTopic.id) {
<option [value]="subTopic.id">{{ subTopic.name }}</option>
}
</select>
</div>
}
</form>
`,
})
export class AddQuestionComponent {
private readonly topicService = inject(TopicService);
defaultControls = {
title: signal(''),
topic: signal<number | null>(null),
};
hasSubtopic = computed(() => {
return !!this.topics().find(
(topic) => topic.id === +(this.defaultControls.topic() ?? 0)
)?.hasSubtopic;
});
form = computed(() => {
return ({
title: this.defaultControls.title,
topic: this.defaultControls.topic,
...(this.hasSubtopic() ? { subtopic: signal<number | null>(null) } : {}),
});
});
topics = toSignal(this.topicService.getTopics(), { initialValue: [] });
subTopics = toSignal(
toObservable(this.defaultControls.topic).pipe(
filter((topicId) => this.hasSubtopic()),
switchMap((topicId) => this.topicService.getSubTopics(topicId!))
),
{ initialValue: [] }
);
}
A computed value lets you shape the form based on the current selection. The reactive equivalent is markedly more involved:
@Component({...})
export class AddQuestionComponent implements OnInit {
private readonly topicService = inject(TopicService);
form = new FormGroup({
title: new FormControl(''),
topic: new FormControl<number | null>(null),
});
ngOnInit() {
// here, we need to separately subscribe to form changes
// and add/remove the control manually
this.form.get('topic')?.valueChanges.subscribe((topicId) => {
if (
topicId &&
this.topics.find((topic) => topic.id === topicId)?.hasSubtopic
) {
this.form.addControl('subtopic', new FormControl(null));
} else {
this.form.removeControl('subtopic');
}
});
}
topics = toSignal(this.topicService.getTopics(), { initialValue: [] });
subTopics = toSignal(
toObservable(this.form.get('topic')?.value).pipe(
filter((topicId) => this.form.get('subtopic')?.value),
switchMap((topicId) => this.topicService.getSubTopics(topicId!))
),
{ initialValue: [] }
);
}
That logic is far from elegant, and the complexity only grows as more controls are added.
The Reactive Forms API also leans imperative. Enabling or disabling a field means calling disable() or enable() on the control. There is no template binding to a property like there is with template-driven forms. Toggling a disabled state based on a stream often requires a subscription and manual updates, which can lead to messy, hacky code.
Making the Call?
Honestly, a lot of this is preference, but consistency matters more than the specific pick. You have a few solid paths:
- Commit to Reactive Forms and don’t look back.
- Commit to template-driven forms and keep it uniform.
- Use template-driven forms for straightforward cases with clear boundaries (like no custom validations), and bring in Reactive Forms only for intricate setup with many inputs and overlapping rules.
That covers forms — now for the last stop, which might not even be a real rivalry.
RxJS versus Signals
RxJS has been a staple since the early days of Angular, while signals are the new kid on the block. The introduction of signals has reignited the debate, with some developers eager to leave RxJS behind. But is that rush justified, or is it a knee-jerk reaction?
RxJS
Advantages:
RxJS is a heavyweight when it comes to power and flexibility. It is deeply woven into the framework — take HttpClient and the Router as prime examples. The vast operator library covers transformation, combination, and timing with finesse. Done right, the code is clean, readable, and maintainable. It is equally at home with sync and async logic, making it a jack of many trades.
Drawbacks:
The sheer scope is intimidating. Over a hundred operators, cold and hot Observables, and the ever-present risk of memory leaks from mismanaged subscriptions — it’s a lot. As a standalone library, it also invites debate on best practices, from “subscribe or use the async pipe?” to endless variations on approach.
RxJS is not the answer to everything. For straightforward synchronous flows, it can be overkill, which is exactly where signals show their strength.
Signals
Advantages:
Signals are refreshingly simple. Create one, derive another, compose them, or watch for changes with an effect. Their predictability is a big plus; reasoning about them feels intuitive, and there’s less room for misuse than with RxJS.
Drawbacks:
They are synchronous by nature, which cuts both ways. On the bright side, you can read the value without ceremony and forget about race conditions. But async needs — like fetching data over HTTP — don’t go away, and signals aren’t built for those heavy-lifting use cases by design.
There’s also no operator-rich toolbox for timing, filtering, or complex composition; that responsibility falls back on the developer.
So, where does that leave us?
Choosing a Path
This section deserves a closer look than the others.
First off, is there real friction between signals and RxJS? Not necessarily. Signals shine when dealing with reactive values — state that we read and update, while tracking changes. RxJS owns the world of events: browser interactions, HTTP calls, storage updates, Web Sockets. Its operators give us control over both what gets emitted (via filter, map, or combineLatest) and when (with debounceTime, timer, or interval).
They serve different purposes, but their paths cross. An HTTP request is an event — great for RxJS. The result is data we might want to hold on to — a perfect fit for a signal. Ideally, both should coexist and interoperate seamlessly.
And that’s exactly what the Angular team has been enabling. Even as they trim the framework’s direct RxJS dependency, they’ve introduced "@angular/core/rxjs-interop", a package that bridges the gap between signals and Observables for concise, yet powerful code.
A quick look at what that feels like:
@Component({
template: `
<input type="text" [(ngModel)]="query">
<ul>
@for(product of products(); track product.id) {
<li>
{{ product.name }} - {{ product.price }}
</li>
}
</ul>
`,
imports: [
FormsModule,
],
})
export class ProductListComponent {
private readonly productService = inject(ProductService);
query = signal('');
// extract the result of the HTTP call into a signal
products = toSignal(
// switch to Observable world
toObservable(this.query).pipe(
// use the power of RxJS operators to introduce timing limits
debounceTime(500),
switchMap(query => this.productService.getProducts(query)),
),
{ initialValue: [] },
)
}
Notice how blending RxJS with signals gives us a clean abstraction for a timed HTTP request. The end result is simply a signal we can use in the template or elsewhere in the component, without exposing the inner machinery.
So, is there a real conflict? I’d argue there isn’t. Here’s how to approach the choice:
- Use both. It’s straightforward, but it means learning the ropes for each.
- Stick with RxJS, especially if you’re on an older Angular version without signals. The good news is that the Observable pattern will make the eventual transition to signals smoother.
- Go signals-only if you’re committed to avoiding RxJS. The framework is moving toward making RxJS optional (and better supported), and this works for simpler apps.
I’ve left out “avoid both” on purpose. That’s usually a coincidence of legacy code rather than a deliberate decision.
Whatever you pick, remember RxJS isn’t going away. If anything, it’s getting better integrated with the framework.
Wrapping Up
Just like any technology, Angular is shaped by the people using it, and people can have strong, varied opinions. These differences are inevitable but shouldn’t define the framework’s value or stop us from exploring new ideas and changing our minds. For those just starting out, all the debate can feel like a lot to process. My hope is that this article has given you a clearer lens to make your own thoughtful decisions.
A Small Plug
You’ve probably noticed that some items we covered — like inject and signals — are recent additions to Angular. That wave of changes has left many developers unsure about which tools to reach for and how to migrate existing projects. I have something that might help: my first book is about to be published.
It’s called “Modern Angular,” a full tour of the great features introduced across versions v14 to v18, like standalone components, improved inputs, signals (naturally!), better RxJS interop, SSR, and more. If that sounds useful, you can find it here.
Right now the book is in copy-editing, with the official release not far off. All 10 chapters are already up in Early Access. For updates on the print version, follow me on Twitter or LinkedIn for announcements and promotions.
P.S. Chapter 5 digs into RxJS and Angular interoperability, and chapters 6-7 go deep on signals ;)

