Understanding @ngrx/component-store Selector Debouncing

Stephen Cooper - Oct 6 '20 - - Dev Community

@ngrx/component-store selectors have a debounce option that lets the state settle before emitting. But what does this mean and how does it work?

NgRx Component Store

I have started using @ngrx/component-store to manage component state in my applications and so far I am loving it! In this post I am not going to explain how or why to use @ngrx/component-store but if you want to know more check out this video by Alex Okrushko.

Debounce Selectors

In this post I want to take a closer look at the {debounce} config option for the select method. Here is what the docs say about debouncing.

Selectors are synchronous by default, meaning that they emit the value immediately when subscribed to, and on every state change. Sometimes the preferred behavior would be to wait (or debounce) until the state "settles" (meaning all the changes within the current microtask occur) and only then emit the final value. In many cases, this would be the most performant way to read data from the ComponentStore, however its behavior might be surprising sometimes, as it won't emit a value until later on. This makes it harder to test such selectors.

At first I did not understand what this meant so I built an example in Stackblitz to see what difference the flag made to a selector.

Demo App Setup

We setup the component store as part of the AppComponent with a boolean toggle state.

interface AppCompState {
  toggle: boolean;
}
Enter fullscreen mode Exit fullscreen mode

We then create two selectors on this toggle, one which we debounce and the other that we do not.

update$ = this.select((s) => s.toggle, { debounce: false });

updateDebounced$ = this.select((s) => s.toggle, { debounce: true });
Enter fullscreen mode Exit fullscreen mode

As the docs speak about selectors being synchronous I have created two methods that watch the toggle state and then toggle it back. A bit like a naughty child turning the TV back on as soon as you turn it off!

The important difference is that we include a delay(0) in the second toggler to make the toggleState call asynchronous.

// Set up synchronous auto toggle back
this.select((s) => s.toggle)
  .pipe(take(1))
  .subscribe(() => this.toggleState());

// Set up asynchronous auto toggle back using delay(0)
this.select((s) => s.toggle)
  .pipe(delay(0), take(1))
  .subscribe(() => this.toggleState());
Enter fullscreen mode Exit fullscreen mode

We trigger these actions by two different buttons in the demo app.

Synchronous Updates

When we click on Update Sync only the selector with debounce: false emits any values. Without debouncing the selector emits every changed toggle value.

Synchronou Updates

However, the selector that is debouncing emits no change. Why is this? The value of the toggle starts as true, gets set to false before being set back to true. This all happens synchronously, (in the same microtask) and is debounced by the debounceSync function. At the end of the microtask the value is still true and the selector does not emit. There is a distintUntilChanged in the select method that ensures this.

Asynchronous Updates

When we click on Update Async both selectors now emit values. The debounceSync function, as the name suggests, only debounces synchronous updates. Now the debounced selector emits every toggle change as each occurs in a different microtask.

Asynchronous Update

What does this all mean?

Perfomance

As the docs suggest using debounce: true can improve the performance of your app as the selectors will only emit new values at the end of a microtask. In our demo app this means the selector would not emit at all resulting in no further actions / re-rendering. Debouncing avoids unnecessary work.

Consistency

State emitted by a debounced selector may be more consistent or logically correct. For example, if the selector relies on multiple properties, which are interdependent, then we want them to have reached a valid state before the selector emits. Setting {debounce:true} ensures we do not emit all the intermediary values which could originate from a temporary 'invalid state'.

Next steps

In my next post we will examine the debounceSync source code to see how this debouncing actually works.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .