/**
 * Written by Tycho on June 19, 2025
 *
 * @param tags angular, best practice
 */

Reactive Thinking (in Angular)

Signals are gaining momentum.

Since Angular introduced signals as their new reactivity model, it’s been a focal point of discussion in the community. Developers are excited. Tutorials are everywhere. It feels like a revolution.

Signals offer a simpler, more direct way to express reactivity. They lower the barrier to entry.

But, the real win isn’t signals. It’s thinking reactively.

Reactive patterns have always been part of Angular. RxJS has been baked into the framework from the start; and for good reason. They help manage complexity, model time and data flow, and keep code predictable even as the app scales.

The concepts are the same. The syntax has simply become easier. So, instead of asking, “Should I use signals or RxJS?”, start asking:

“How can I think (more) reactively?”

Imperative patterns

To better understand what reactive thinking entails, let’s first look the opposite: imperative programming.

Imperative programming is all about manual control. You fetch data, handle events, and push values into the DOM or state.

// Imperative using static properties
@Component({
template: `<h1>Welcome, {{ userName }}</h1>`,
})
export class UserComponent {
readonly #userService = inject(UserService);
userName?: string;
isLoading = true;
ngOnInit() {
this.#userService.getUser().subscribe((user) => {
this.userName = user.name;
this.isLoading = false;
});
}
}

Recognize this pattern? You’re telling the app exactly how to operate: what to do, and when to do it. You’re in control of the flow.

Imperative programming, especially in large apps using complex data flows, quickly leads to several problems:

Reactive patterns

Reactive programming, on the other hand, is about relationships.

You declare what something depends on, and the system updates automatically when those dependencies change.

// Reactive using RxJS
@Component({
template: `<h1>Welcome, {{ userName$ | async }}</h1>`,
})
export class UserComponent {
readonly user$ = inject(UserService).getUser();
readonly userName$ = this.user$.pipe(map((user) => user.name));
readonly loading$ = this.user$.pipe(mapTo(false), startWith(true));
}

Or, using the shiny new Signal APIs:

// Reactive using signals
@Component({
template: `<h1>Welcome, {{ userName() }}</h1>`,
})
export class UserComponent {
readonly user = toSignal(inject(UserService).getUser());
readonly userName = computed(() => this.user()?.name);
readonly loading = computed(() => !this.user());
}

Notice how there’s no manual coordination: the state updates itself. You define the what, and the how happens behind the scenes.

Front-ends are reactive by nature:

Trying to manage that imperatively often leads to bugs, complexity, and fragile code. But, following reactive patterns makes your code more responsive, composable and testable.

How do you think reactively

So, you are completely convinced about benefits of using reactive patterns, but how do you move away from the imperative mindset?

Here are four tips to start thinking more reactively.

1. Think in terms of relationships

To think reactively, start by shifting your mindset from steps to relationships. Reactive thinking is less about handling events, and more about expressing dependencies.

Instead of asking “How do I make the program handle events and update state accordingly”, start asking:

“What should this value depend on?”

In other words, you model relationships between data instead of sequencing steps in a program.

2. Only use reactive primitives for state

Stop using static class properties for state.

Store your state in reactive primitives: signal, Subject, BehaviorSubject, or Observable. These act as your sources. Then derive new values using computed() (with signals) or RxJS operators like map and switchMap.

This approach lets your app react automatically to changes, keeping your logic clean and declarative.

3. Build from sources

Build your data streams from sources: things like route params, service calls, or form input values.

Let the rest of your state be derived from those. Keep asking “What does this value depend on?”, until you reach the source of your data stream.

For example, if we want to create a component that shows a user’s name, using an ID passed in the URL, we would use the following steps:

Here, we start by defining the source (route parameter), and derive the state accordingly:

@Component()
export class UserComponent {
readonly #userService = inject(UserService);
// Source: route parameter stream
readonly userId$ = inject(ActivatedRoute).paramMap.pipe(
map((params) => params.get('id')),
);
// Derived: user observable based on route param
readonly user$ = this.userId$.pipe(
switchMap((id) => this.#userService.getUserById(id)),
);
// Derived: user signal based on the user Observable
readonly user = toSignal(this.user$);
// Derived: user name signal taken from the user object
readonly userName = computed(() => this.user()?.name);
}

NOTE

Notice that we are converting an observable user$ to a signal using the RxJS interop helper toSignal. To learn more about how RxJS and signals work together, check the official docs.

4. Avoid manual state reads

Manual subscriptions (using subscribe()), or using effect() with signals, often lead back to imperative thinking: you fetch data, assign it to a property, and manage cleanup manually. This breaks the reactive flow and spreads logic across lifecycle hooks and event handlers.

Instead, keep your data in reactive primitives, and let the framework handle updates and teardown.

Use the async pipe in templates with observables:

<h2>{{ userName$ | async }}</h2>

Or use signals directly:

<h2>{{ userName() }}</h2>

This keeps your code declarative, consistent, and reactive.

TL;DR

Reactive thinking means modeling relationships between data instead of managing steps manually.

Whether you’re using RxJS or Signals, the goal is the same: write declarative, maintainable, and responsive code by thinking reactively.

TIP

Added bonus: following reactive patterns, and storing data in reactive primitives, makes your applications easier to migrate to a zoneless future of Angular.