Reactive Search Feature with Angular

Imab Asghar
Code道
Published in
4 min readNov 3, 2020

Every application we build will most likely include a search feature. Developers will build this differently based on their experience. Some will build reactively where as some will build non reactive (hint: (change)=”search()”). I will try to write reactively and simplify our observables down for different features within the search feature itself. Down below is a simplistic demo of what a search feature looks like.

A component with reactive version would look something like below:

@Component()
export class SearchComponent implements OnInit {
public searchControl: FormControl;
public searchResults$: Observable<SearchResult[]>;
constructor(
private searchService: SearchService,
private formBuilder: FormBuilder,
) {}
ngOnInit() {
this.searchControl = this.formBuilder.control('');
this.searchResults$ = this.searchControl.valueChanges
.pipe(
switchMap(searchString => this.searchService.search(searchString))
)
}
}

Now we have two options. Either subscribe and unsubscribe in the component or use the async pipe.

However the search feature is not limited to just this main feature. There are other small features (UX) that will often popup when you are building a search feature. For example start searching only if minimum number of characters have been typed. This is to avoid having too many results which don’t add value to the user. The primary instinct is to just add a filter in the pipe like this:

this.searchResults$ = this.searchControl.valueChanges
.pipe(
filter(searchString => searchString.length >= 3),
switchMap(searchString => this.searchService.search(searchString))
)

On the contrary, there is a bug introduced now. When I type in 3 characters and then remove a character it will still show you the last search results for 3 characters. To fix this bug, you can remove the filter and change the switchMap to use the javascript ternary operator along with rxjs of operator:

this.searchResults$ = this.searchControl.valueChanges
.pipe(
switchMap(searchString => searchString.length >= 3 ? this.searchService.search(searchString): of([]))
)

This is good enough for functionally but it is still not good enough for UX since you want to have the following messages in the template depending on the state of the search:

  • “Enter minimum 3 characters to start searching”. You want this message to be showing until the user has typed in 3 characters in the search field.
  • “No results found”. In case the search string does not match with any search results.

To handle these two user experience it becomes hard with the current observable we have. One option is to switchMap to of(null) and in the template you can check if null show the “Min 3 character” message. However if it is an empty array (searchResult.length === 0) you can show the “No Results found” message.

To me this is not satisfying enough. If I need to add another UX in it sometime later, I will try to patch it in the switchMap. So I want to refactor it. I think there is a chance we can leverage separation of concern design principle to this code:

this.areMinimumCharactersTyped$ = this.searchControl.valueChanges
.pipe(
map(searchString => searchString.length > 3)
)
const searchString$ = merge(
defer(() => of(this.searchControl.value)),
this.searchControl.valueChanges,
)
this.searchResults$ = searchString$
.pipe(
switchMap((searchString: string) =>
this.searchService.search(searchString)
),
share() // To share with areNoResultsFound$ and avoid double api calls
);
this.areNoResultsFound$ = this.searchResults$
.pipe(
map(results => results.length === 0)
);

Explanation for the searchString$:

I need to get the initial value since valueChanges would only trigger for changes. So why use defer instead of startWith? In the template searchResults$ observable is invoked after minimum characters are typed and startWith will always take the initial empty string (‘’) value instead of the current value. This will make more sense when looking through the template further down.

Additionally since I need to optimize and reduce api calls, I can:

  • add debounceTime to start searching once the user has finished typing so it reduces the api calls.
  • add distinctUntilChanged to only search if the searchString has changed.
const searchString$ = merge(      
defer(() => of(this.searchControl.value)),
this.searchControl.valueChanges,
).pipe(
debounceTime(1000),
distinctUntilChanged(),
);
// Other options for adding these two operators:
// - In the this.searchResults$ variable
// - create a new variable called distinctDebouncedSearchString$

Use these observables in the template with async pipe:

<ng-container
*ngIf="areMinimumCharactersTyped$ | async; else lessThanMinimumCharactersTyped"
>
<ng-container *ngIf="areNoResultsFound$ | async; else results">
No Results found.
</ng-container>
<ng-template #results>
Got results
<ul>
<li *ngFor="let result of searchResults$ | async">
{{ result | json }}
</li>
</ul>
</ng-template>
</ng-container>
<ng-template #lessThanMinimumCharactersTyped>
Enter minimum 3 characters to start searching.
</ng-template>

“I hope someone could show me a better way to write the template.” — Desperate Author

Additionally if I use ngrx and effects to make the api calls, I can easily handle the loading, success and error states and show UI accordingly. Although I would have to change my search component slightly. How to use ngrx and effects with exposing the necessary UI states is explained well in another one of my blogs: “Handling Success/Failure/Loading when calling an API with Angular and ngrx”.

Although I ended up writing more code, however I think I was able to write cleaner, more maintainable code with more Domain Specific Language (DSL) and simplifying each observable to single responsibility. Overall it would be easier to add more features later down the track and people would avoid the pitfall of technical debt.

Here is the final version on stackblitz.

Thanks for reading!! Feel free to comment or ask any questions.

--

--