/** * 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:
- Scattered updates make state hard to track. A property can be changed from multiple places, making it unclear where and when updates happen.
- Manual control increases complexity. Handling every step of data flow manually often results in tightly coupled and logic that is hard to maintain.
- Keeping your interface in sync is fragile. Forgetting to update the UI when data changes can cause bugs and inconsistencies.
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:
- User interactions cause side effects.
- The interface reflects state.
- State changes over time.
- Changes ripple across components.
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:
- The user’s name depends on the API response for the current user.
- The API response depends on the current ID from the URL.
- The current ID comes from the route parameter ‘id’.
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.
- Use reactive primitives (
signal
,computed
, RxJS Observables) for storing state. - Start from sources (e.g., route params, inputs, API calls).
- Avoid imperative code like
subscribe()
or manual property updates. - Let the framework handle updates via the async pipe or signals in templates.
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.