This content originally appeared on HackerNoon and was authored by Jack Baker
In Angular development, RxJS has long been a go-to tool for managing asynchronous data and complex event streams. With the recent introduction of Signals, Angular now offers even more options for reactive programming. Signals let us declare a reactive state in a way that automatically updates templates whenever the state changes. Angular also provides an interop layer between Signals and RxJS, allowing us to seamlessly combine the flexibility and power of RxJS with the simplicity and performance of Signals.
\ When working with RxJS in Angular, there are two main coding approaches: imperative and declarative. In this post, we’ll explore these styles with a practical example of fetching and filtering a list of fruits.
Imperative vs. Declarative Programming
- Imperative programming focuses on how to do things by giving detailed instructions to the program. It often involves direct control of variables and mutable state.
\
- Declarative programming focuses on what outcome is needed, abstracting away some of the specifics on how to achieve it. In Angular, RxJS operators enable a declarative style that leads to simpler, more maintainable code.
Scenario: Filtering a List of Fruits
In this example, we will use a list of fruits retrieved from a service that a search input can filter.
Here is the code for our fruit service:
export interface Fruit {
name: string;
colour: string;
}
@Injectable({
providedIn: 'root',
})
export class FruitService {
private fruits$ = new BehaviorSubject<Array<Fruit>>([
{ name: 'Apple', colour: 'Red' },
{ name: 'Banana', colour: 'Yellow' },
{ name: 'Orange', colour: 'Orange' },
{ name: 'Grapes', colour: 'Purple' },
{ name: 'Pineapple', colour: 'Brown' },
{ name: 'Strawberry', colour: 'Red' },
{ name: 'Watermelon', colour: 'Green' },
{ name: 'Blueberry', colour: 'Blue' },
]);
public getFruits(): Observable<Array<Fruit>> {
return this.fruits$.asObservable();
}
}
Our component template code will remain the same between the two examples, here is the template code:
<input placeholder="Filter fruits" ngModel (ngModelChange)="setSearchValue($event)"/>
<ul>
@for (fruit of filteredFruits(); track fruit.name) {
<li>{{ fruit.name }}</li>
}
</ul>
Imperative Approach
In an imperative approach, we directly mutate state and have to manually manage subscriptions. This is how it could look:
@Component({
selector: 'app-imperative',
standalone: true,
templateUrl: './imperative.component.html',
imports: [
FormsModule
]
})
export class ImperativeComponent implements OnInit, OnDestroy {
private readonly _fruitService: FruitService = inject(FruitService);
private readonly _subscriptions: Subscription = new Subscription();
private readonly _searchValue$: BehaviorSubject<string> = new BehaviorSubject('');
private _fruits: Array<Fruit> = [];
public readonly filteredFruits: WritableSignal<Array<Fruit>> = signal([]);
public ngOnInit(): void {
this._subscriptions.add(
this._fruitService.getFruits()
.subscribe({
next: (fruits) => {
this._fruits = fruits;
this.filteredFruits.set(fruits);
}
})
);
this._subscriptions.add(
this._searchValue$
.pipe(
debounceTime(300),
distinctUntilChanged(),
map((searchValue) => this._fruits.filter((fruit) => fruit.name.toLowerCase().includes(searchValue.toLowerCase()))),
)
.subscribe({
next: (filteredFruits) => {
this.filteredFruits.set(filteredFruits);
}
})
);
}
public ngOnDestroy(): void {
this._subscriptions.unsubscribe();
}
public setSearchValue(value: string): void {
this._searchValue$.next(value);
}
}
In this imperative code:
We fetch the list of fruits in
ngOnInit
and save it in the private_fruits
field.\
We update the
filteredFruits
array whenever the input changes, manually filtering the list based on the current filter text.\
We handle subscription cleanup in
ngOnDestroy
to avoid memory leaks.
\ While this works, directly managing the state and subscriptions manually can make the code harder to read and maintain and will only get worse as the application grows. In total, this solution is 50 lines long.
Declarative Approach
Now, let’s rewrite this example using a declarative style with RxJS. This approach relies on various streams which are then converted to signals for use within the template.
@Component({
selector: 'app-declarative',
standalone: true,
templateUrl: './declarative.component.html',
imports: [
FormsModule
]
})
export class DeclarativeComponent {
private readonly _searchValue$: BehaviorSubject<string> = new BehaviorSubject('');
private readonly _fruitService: FruitService = inject(FruitService);
private readonly _fruits$: Observable<Array<Fruit>> = this._fruitService.getFruits()
.pipe(
shareReplay(1)
);
private readonly _filteredFruits$: Observable<Array<Fruit>> = combineLatest([this._fruits$, this._searchValue$])
.pipe(
debounceTime(300),
distinctUntilChanged(),
map(([fruits, searchValue]) => fruits.filter((fruit) => fruit.name.toLowerCase().includes(searchValue.toLowerCase()))),
);
public readonly filteredFruits: Signal<Array<Fruit> | undefined> = toSignal(this._filteredFruits$);
public setSearchValue(value: string): void {
this._searchValue$.next(value);
}
}
Here’s how the declarative approach simplifies things:
We use a
BehaviorSubject
called_searchValue$
to store the current filter text as an observable stream.\
combineLatest
merges thefruits$
stream with the_searchValue$
stream so that whenever either changes, thefilteredFruits$
observable recalculates the filtered list.\
Angular’s
toSignal
interop helper is being used on the publicfilteredFruits
field and converts thefilteredFruits$
stream to a signal so we can use it within our template. This interop function will automatically handle subscribing and unsubscribing to the required streams to fetch the data. This means there is no manual handling of subscriptions within our component!
\ The declarative code focuses on what we want the component to display. Our streams are split and are single responsibility making them small, concise and re-usable. In total, this solution is 28 lines long.
Advantages of the Declarative Approach
Readability: Declarative code is often more readable, especially with RxJS operators like
map
andcombineLatest
. It expresses what we want to happen without detailed instructions on how to do it. You can also see in this particular example the amount of code required in our component was reduced from 50 lines in the imperative example to just 28 in the declarative example.\
Automatic Subscription Management: The
toSignal
interop helper in Angular automatically manages subscriptions to our streams, reducing the risk of memory leaks.\
Predictability: The template updates reactively whenever the data or filter text changes, making the flow of data and updates more predictable.
Disadvantages of the Declarative Approach
Higher Abstraction Complexity: Declarative code abstracts away the how, focusing instead on the what. While this improves readability, it can make debugging more challenging, especially for developers unfamiliar with RxJS operators or reactive programming concepts.
\
Steeper Learning Curve: For developers new to RxJS or functional programming, the declarative style can be difficult to grasp. Operators like
map
,switchMap
, andcombineLatest
require a solid understanding of observables and stream transformations.
Conclusion
The declarative approach to RxJS in Angular offers a clean, reactive way to handle asynchronous data flows. While the imperative style works, it can become challenging to manage as applications grow more complex.
\ If you’re working with RxJS in Angular, try adopting a more declarative style, and see how it can improve your code!
\ Copyright © 2024 Jack Baker
This content originally appeared on HackerNoon and was authored by Jack Baker
Jack Baker | Sciencx (2024-11-08T01:37:04+00:00) Angular: Exploring Imperative and Declarative Programming with RxJS and Signals. Retrieved from https://www.scien.cx/2024/11/08/angular-exploring-imperative-and-declarative-programming-with-rxjs-and-signals/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.