Check out myAngular article series with live demos and Facebook group Angular - Advanced Topics

Creating a Search Filter in React and RxJs

Torgeir "Tor" Helgevold
- JavaScript Developer and Blogger
Published: Sat Jul 08 2017

In this post I will show how to create a type to search filter using React and RxJs.

On the surface it might seem trivial to create a type to search filter. Isn’t it just a matter for submitting search strings to an API? What could possibly go wrong?

Actually it’s a bit more complicated than that.

The biggest challenge is dealing with out of order request execution if there are multiple searches in flight at once.

How can you have multiple searches in flight at the same time?

This is very likely to happen if the filter issues new searches continuously as we are typing.

Let’s say we are using the filter to search for “Card”. As we are typing, the filter may send off a search for “Car”, and a second search for “Card” milliseconds later.

There is no guarantee that the search for “Car” will finish before the search for “Card”. As a result we may get the results back out of order. The effect of this would be that we see the wrong search results in the filter.

How can we solve this problem?

The best way to handle this is to cancel the search for “Car” as soon as we detect a newer search for the term “Card”.

Let’s discuss a few common http implementations.

Promises

Cancelling promise based http requests is not trivial. There is no built in mechanism for promise cancellation. You can build your own implementation of promise cancellation, but this will definitely add to the complexity of your http calls.

Observables

Instead you should use observables. One of the key strengths of observables is built in support for cancellation.

In the following example I will show how to take advantage of this by using RxJs observables to cancel stale searches “in flight”.

I should clarify that “cancellation” here is referring to cancelation from a client side perspective. We will not be able to prevent an http request from hitting the server if it was already sent out by the browser.

Think of “cancellation” as ignoring responses from cancelled http requests, which is key to ensuring correct execution order.

Now, let’s look at the search filter implementation.

The code consists of a React component and an RxJs based service.

search-component.js
import React, { Component } from 'react'; import { SearchService } from './search-service'; class Search extends Component { constructor() { super(); this.searchService = new SearchService(); this.state = {results: []}; } componentDidMount() { this.searchService .getResults() .subscribe(res => { this.setState({results: res}); }); } search(event) { this.searchService.search({value: event.target.value.trim()}); } render() { let results = this.state.results.map(res => { return <li className="list-group-item" key={res.title}> <a href={res.friendlyUrl}>{res.title}</a> </li> }); return ( <div className="form-group"> <h4>Search</h4> <input className="form-control" placeholder="Search Term" type="text" onChange={this.search.bind(this)} /> <ul className="list-group"> {results} </ul> </div> ); } } export default Search;

The component displays a simple search input box. The captured input is then passed to the service in order to trigger searches.

The other part is a subscription to an observable returned from the service. By subscribing to the observable we get notified every time there is a new search result to display.

The majority of the search logic is found in SearchService.

In the next section we will go through the code in more detail, and explain how RxJs makes it super easy to build a reliable search feature.

search-service.js
import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; import 'rxjs/add/observable/of'; import 'rxjs/add/observable/fromPromise'; import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/switchMap'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/filter'; export class SearchService { constructor() { this.searchTerm = new Subject(); } search(term) { this.searchTerm.next(term.value); } doSearch(term) { let promise = fetch(`${window.ctx.searchUrl}/${term}`) .then(response => response.json()); return Observable .fromPromise(promise) } getResults() { return this.searchTerm .debounceTime(500) .distinctUntilChanged() .switchMap(term => term ? this.doSearch(term) : Observable.of([])) .catch(error => { console.error(error); return Observable.of([]); }); } }

Most of the implementation boils down to composing a chain of RxJs operators.

The first step is to start the observable chain. In this case we are capturing user input before mapping it to an observable via an RxJs Subject. For every typed value a new value is emitted by the Subject.

Next we debounce typed values by 500ms to prevent slamming the API with partial search values. There is no need to send off searches for every key stroke if the user is still typing.

We are also adding distinctUntilChanged() to prevent the search from firing unless the search value actually changed since the previous search. This is helpful if someone changes the search term, only to quickly change it back within the debounce window.

Where does the cancellation happen?

SwitchMap is the key to cancelling observables. Actually switchMap serves two roles:

First we use it to chain our input observable with an observable that wraps the actual search API http request.

However, before processing the http observable, switchMap will check if there are other http observables “in flight”. If there are, switchMap will cancel processing of previous http observables.

This is important since it makes sure that only the latest observable will be processed. All previous observables are discarded.

Demo

To see this implementation in action, check out the new search feature for SyntaxSuccess.

Test it out by searching for React :-)

If you liked this article, share it with your friends on social media:

We also have a new Facebook group about advanced Angular topics.

I invite you to follow me on twitter