Stimulus and RxJS For Improved Loading States

I’ve written before about using Stimulus and RxJS, but I wanted to write again about an improved way of handling loading states using RxJS and Stimulus.

Displaying a loading state is a great way to indicate to the user that there is something happening in response to a button click, but sometimes if the user is on a fast connection or machine, displaying that loading state too quickly can have a negative impact.

For example, if a user action takes ~100ms to process something, displaying and then quickly hiding a loading state will just show a flash of content and could leave the user a little bit confused.

Fortunately, RxJS makes handling this extremely simple to implement. In the previous post we implemented this using setTimeout and clearTimeout, but now we’ll see how this can be improved by using RxJS primitives.

Loading State

A loading state is something we want to display when the user clicks an action and the application needs to retrieve data from a remote source. Since we don’t know how long that data retrieval will take place, we display a loading state letting the user know something is happening.

Here is an example of how this could look in response to a button click

setupClickEvent() {
  this.newClick
    .pipe(
      tap(() => {
        this.displayLoadingState();
      }),
      delay(500)  // Fake delay
    )
    .subscribe(() => {
      this.displayContent();
    });
}

displayLoadingState() {
  this.loadingTarget.classList.remove('hidden');
}

displayContent(content) {
  this.loadingState.classList.add('hidden');

  // display actual content
}

If you’ve used RxJS before (or read the previous post), this should be pretty familiar. When a new click is observed, we immediately display the loading state. Once the data is retrieved from the remote source, we hide the loading state.

Adding a Loading Delay

To improve the experience, so we don’t get a flash of a loading indicator for users on a fast connection, we should add a delay between the time that the loading takes place and when we actually modify the DOM to do display it. Basically, we separate the loading state from the logic of actually displaying the loading state.

To do this, we first want to add a new Subject for handling the loading state.

import { Controller } from 'stimulus';
import { Subject } from 'rxjs';
import { tap, delay, filter, debounceTime } from 'rxjs/operators';

export default class extends Controller {
  static targets = ['loading', 'delay'];

  newClick = new Subject();
  isLoading = new Subject();

  loadingDelay = 350;

  connect() {
    this.setupClickEvent();
    this.setupLoadingState();
  }

  myClick(event) {
    this.newClick.next();
  }

  setupClickEvent() {
    this.newClick
      .pipe(
        tap(() => {
          this.delayTarget.innerText = '';
          this.isLoading.next(true);
        }),
        delay(150)
      )
      .subscribe(() => {
        this.isLoading.next(false);
        this.delayTarget.innerText = `Data available`;
        // Display content
      });
  }

  setupLoadingState() {
    // Subscribe to when loading state is enabled
    // but wait 250ms before updating the DOM and displaying it
    this.isLoading
      .pipe(
        debounceTime(this.loadingDelay),
        filter(value => value === true)
      )
      .subscribe(() => {
        this.loadingTarget.classList.remove('hidden');
      });

    // Subscribe to when loading state is disabled
    // and hide it immediately
    this.isLoading
      .pipe(
        filter(value => !value)
      )
      .subscribe(() => {
        this.loadingTarget.classList.add('hidden');
      });
  }
}

The bulk of the work takes place in the handleLoadingState method and is done completely with RxJS primitives filter and debounceTime. What we do is create two subscriptions - the first for when loading state is enabled, and the second for when the loading state is turned off.

The difference between the two is that we add a debounce between the time the loading state is turned on and the time that we actually display it. If the loading state is disabled between that time, it will fail the filter check and the loading state stays hidden.

We also updated our button click code to simply call next on the isLoading subject with the appropriate state (true or false). This further decouples the displaying of the loading state from the retrieving of the data. All the retrieving of the data needs to do is tell RxJS that something is loading or is done loading. Everything else happens by subscribing to that Subject.

If we change the fake delay on our button click action to be higher than 350, we’ll see the loading indicator display. If we change the fake delay to less than 350, we won’t show the loading indicator at all.

Conclusion

With just a few more lines of code, we’ve improved our loading state logic quite a bit. We’ve decoupled the loading state itself from the logic that handles the display of the loading state.

If you’d like to see the final code, please feel free to browse the mike1o1/stimulus-rxjs-loading-state repository.

comments powered by Disqus