@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;
}
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 });
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());
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.
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.
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.
Top comments (3)
If you want to see the debounce flag in action in a larger example take a look at this Stackblitz from the ngrx docs. You can change the value in the paginator.store.ts and see the extra events emitted in the console.
ngrx.io/guide/component-store/usag...
stackblitz.com/angular/dkqxkdapgqj...
How did you set up Unit Test to run {debounce:true} scenario
I am not 100% sure what you mean. I did not use a unit test just changed the flag in the code example and inspected the console output which showed a difference.