Eddie Hinkle

Login

Thinking through Reactive Programming using RxJS

State can be a hard, difficult and bug inducing thing to track and manage. The best way to simplify state is to reduce it. Because of that, at my job, we try to embrace using Observable streams whenever we can. The difference between imperative programming and reactive (using Observables) programming is that in imperative programming you have to continually add a lot of logic around state and determining when and how state changes and making sure that state is reflected properly in the UI.

Reactive programming, focuses on streams of data (in our case called Observables). These streams of data can come from anywhere: mouse event, keyboard event, network request or even a timer running in the background. These streams can trigger events, return data, combine streams, split streams and transform existing data into new shapes and forms. But these streams (much like real life streams of water) have singular sources that are far away up at the top of the mountain. As such, when you are down stream, you aren’t having to make sure the water keeps running, you just install a hydroelectric damn to power your house.

Here is an example, that powers an auto-complete or auto-search function:

  1. this.autoResults$ = merge(of(undefined), fromEvent(this.searchField.nativeElement, ‘input’))
  2. .pipe(
  3. map(e => e !== undefined ? e.target.value : undefined), // Get value from input box
  4. debounceTime(350), // Prevent too many queries back to back
  5. distinctUntilChanged(), // Don’t run duplicate requests
  6. tap(() => { this.loading = true }), // show loading mask, this has to be in curly braces to avoid returning any data
  7. switchMap(searchTerm => this.service.search(searchTerm)), // Search for terms
  8. tap(() => { this.loading = false }),
  9. shareReplay(1) // Prevent different subscribers from creating duplicate parallel async requests
  10. );

The item above starts by merging two Observable streams. The first, is an Observable stream that will emit a single value: undefined. This is for one reason, and one reason alone. On the component loading, we want to immediately kick off a search for all the unfiltered results. So in the example above, undefined means we don’t have any search terms and we just want ALL of the results. It loads on component load, but if we enter keys into the search field, it will automatically update. We don’t have to assign any logic to watching the input field and changing a search term state when it’s been modified and then determining when and how to launch the search logic after the search term state is modified.

Instead, it’s a single stream that starts with events being emitted from the search field, and then all of our logic is built alongside that stream. To simplify things, If we ONLY want search results to show if we have entered search results, then it would look like this:

  1. this.autoResults$ = fromEvent(this.searchField.nativeElement, ‘input’)
  2. .pipe(
  3. map(e => e !== undefined ? e.target.value : ‘’), // Get value from input box
  4. filter(e => e.length === 0) // Don’t proceed if the string is empty
  5. debounceTime(350), // Prevent too many queries back to back
  6. distinctUntilChanged(), // Don’t run duplicate requests
  7. tap(() => { this.loading = true }), // show loading mask, this has to be in curly braces to avoid returning any data
  8. switchMap(searchTerm => this.service.search(searchTerm)), // Search for terms
  9. tap(() => { this.loading = false }),
  10. shareReplay(1) // Prevent different subscribers from creating duplicate parallel async requests
  11. );

Here, we start with an event, the search field’s input. We then pipe it through a series of filters, actions and transformations.

Step 1: Map. we convert the data object from a keyboard event into the value of the search field. If the event is undefined for some reason, we get an empty string.

Step 2: Filter. We filter based on the length of the string. We only want to search IF there are characters in the string. If we wanted we could make this set a minimum character limit to 2 or 3 so you don’t search after just 1 or 2 characters.

Step 3: Debounce. To prevent firing a bunch of requests WHILE we are typing the term, we only let the last emitted value through within a given time period. So in this case if during 200ms you type “Hello”. There were 5 emitted values in the stream.

H

He

Hel

Hell

Hello

In this instance, at 350ms, a single value is emitted, which is the last value received: “Hello”.

Step 4: DistinctUntilChanged. Suppose your search term is “Hello”. The results are showing. If you remove the last two characters “lo”, and then within 350ms you will have 4 new values emitted, but only one will make it past the Debounce: “Hello”. This is the same term you already have! Because of that, DistinctUntilChanged will prevent us from running the search logic on the same value.

Step 5: Tap. Tap is essentially to just run an action based on the data coming in. You can use it for all sorts of things: Logging, Analytics, or this case, updating a loading UI.

Step 6: SwitchMap. With a Switch Map we are switching from one observable stream to another in its same place. So in this instance, we are replacing the search term observable with the results from the search service observable.

Step 7: Tap. This tap isn’t called until the step before it is complete, which means when we get here, we have loaded all data from the network, so we can hide our loading UI.

Step 8: ShareReplay. Typically every Observable subscriber is subscribing to their own independent stream of data. Sometimes that is good, but in the regard of a network request like this that is always going to be the same data, it’s better to share the Observable among various subscribers. The “Replay” part of this is the fact that it will return the last value emitted to any late subscribers. This means if one subscriber starts following the stream and the search results happen, but after they are returned another component subscribes to the stream, it will get the last search result that happened (as well as any future results).

This is just a brief overview of one type of reactive/Observable pattern, but it's one I found helpful and thought I would share.

83.9 ℉☀️Frederick, Marylandtechworkreactive-programmingrxjsjavascript
posted using quill.p3k.io
Please note: This site is in an active redesign. Some things might be a little off 🧐