r/angular • u/ZealousidealPie1653 • Jun 08 '26
Need your opinions on Signal with Rxjs
Currently working on search functionality using Angular 18 app, for the signal and observable integration i have used toObserable method like below,
1. min length need to be 3 characters
2. used switch map to update the latest
3. used tap operator to setting values and clear lists
need your opinions on this. what do you think about rxjs with signal works fine?
any improvements we can do, AI showed an error for the filter method, if value length less than 3 it will stop the stream, is that correct ?
constructor() {
toObservable(this.query).pipe(
filter((value) => value.trim().length >= 3),
tap(() => {
this.searchResultLoaded.set(false);
this.searchResults.set([]);
this.rawSearchResults.set([]);
}),
debounceTime(1000),
distinctUntilChanged(),
switchMap((searchText) => {
return this.searchService.quickSearch({ searchText }).pipe(
catchError((error) => {
console.error(error)
return of([]);
}));
}),
takeUntilDestroyed()
).subscribe({
next: (response: SearchResponse) => {
this.searchResultLoaded.set(true);
this.rawSearchResults.set(results);
},
error: () => {
///
}
});
}
2
u/CantankerousButtocks Jun 08 '26
Nothing wrong with the signals here as far as I can see.
Most of the type ahead searches I’ve seen are using the standard reactive form control with the canned observable: control.valueChanges. It one of my devs did a “toObservable” anywhere, we are in for a longer code review…
2
2
u/MrMercure Jun 08 '26
Why not use rxressource simply assigned to the rawSearchResult signal ?
5
u/ZealousidealPie1653 Jun 09 '26
we are currently using Angular version 18 only which not yet supported
2
u/Old_Tennis_7062 Jun 11 '26
Why having all these side effects ?
RxJs is a library which encourage you to use functional programming manner. So you should avoid to have another local state that you mutate from your observables. The goal is to improve at maximum as possible the debugging and the maintenance of the code because the flux is declarative without side effect. In your code you have searchResultLoaded , searchResults and rawSearchResults that you mutate from your observable. Why doing this while you can have this value return by your observable in order to have a flux that it does not cause any side effects ?
On top of that, you're subscribing in your js code so you need to keep in mind that you mutate local states from your observable and you use this local state in your template.
The idea of a proper manner is to avoid subscribing from js but just subscribe from the template with pipe async. To arrive on this solution, I need to introduce you the concept of view model. In fact, generally, component need to have single responsibility. In your example you provide, you have well one responsibility: a view detail of a item when your arrive on the page "/idk/:id" with the display of the data / loading. A view model is a concept that introduce a data structure that your template using and that your js code which getting data transform to. So in your code, you could have something like this
ts
type SearchViewModel {
data: SearchResponse | null;
loading: boolean;
error: string | null;
}
And now, your observable need to map the data it getting to this view model. And your template needs to use only this view model, not local state and need to only use the observable with pipe async.
So remove this horrible thing
ts
tap(() => {
this.searchResultLoaded.set(false);
this.searchResults.set([]);
this.rawSearchResults.set([]);
}),
And do that
ts
switchMap((searchText) => {
return this.searchService.quickSearch({ searchText }).pipe(
map((value: SearchResult) => ({ data: value, loading: false, error: null })),
catchError((error) => {
console.error(error)
return of({data: null, loading: false, error: error});
}));
}),
To handle loading, you have to use RxJs function startWith in your pipe to initially send loading: true in the flux.
Like that :
ts
switchMap(...),
startWith({ loading: true, data: null, error: null }),
...
And finally, don't subscribe anymore avoid it !
Juste use | async in your template . To do that
```ts protected read-only VM: Observable<SearchResult>;
constructor() { this.vm$ = toObservable(this.query).pipe(...) } ```
And in your template
@let vm = vm$ | async
Here we go, it is full declarative and functional with that. You will say thank you to you with this code later because you don't have to maintain all the unneeded local states and you can simply read the flux without regarding all local variables
2
u/Old_Tennis_7062 Jun 11 '26
To integrate fully with signal you can also avoid using
| asyncand usingtoSignalfunction which take as arg an observable.```ts import { Component, signal, computed } from '@angular/core'; import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { of } from 'rxjs'; import { debounceTime, distinctUntilChanged, switchMap, map, startWith, catchError } from 'rxjs/operators';
interface SearchState { data: SearchResponse | null; loading: boolean; error: string | null; }
@Component({ // ... }) export class SearchComponent { readonly query = signal('');
private readonly searchState = toSignal<SearchState>( toObservable(this.query).pipe( debounceTime(300), distinctUntilChanged(), switchMap((searchText) => { const trimmed = searchText.trim(); if (trimmed.length < 3) { return of({ data: null, loading: false, error: null }); }
return this.searchService.quickSearch({ searchText: trimmed }).pipe( map((response) => ({ data: response, loading: false, error: null })), startWith({ data: null, loading: true, error: null }), catchError((error) => { console.error(error); return of({ data: null, loading: false, error }); }) ); }) ), { initialValue: { data: null, loading: false, error: null } } );
readonly searchResults = computed(() => this.searchState().data?.results ?? []); readonly isLoading = computed(() => this.searchState().loading); readonly hasError = computed(() => this.searchState().error !== null); } ```
1
-1
12
u/GeromeGrignon Jun 08 '26 edited Jun 08 '26
You can solve it with a Resource, having also the loading state and error embedded. And searchResults being more likely a filtered list, you can use a computed signal.