Skip to content

component-store: Make selectors debounce better and easier to use #3507

Open
@Harpush

Description

@Harpush

Information

Currently debounce can be used by passing to select { debounce: true }. That means a selector must be either debounced or not.
This creates a few problems:

  1. Any debounced selector cannot be used easily in effects
  2. Any selector based on debounced selector is actually kind of debounced too - but not always how you would expect. That makes even one debounced selector a dangerous thing.

After working with component store for some time I realized what I was missing is having the ability to both debounce and not debounce the same selector. Any selector used in a component should be debounced for performance while selectors used in effects shouldn't. That creates a contradiction with the current implementation as one selector can be used in a component as well as in an effect.

The proposed solution is allowing to debounce every selector while also allow immediate access to any selector. I came up with a solution that wraps component store and I believe it's a good addition. The idea is returning an object from select containing two properties debouned$ and immediate$ allowing the user to decide which strategy to use solving the above problems.

The implementation is pretty straightforward:
First selector:

{
  immediate$: this.select(projectionFn),
  debounced$: this.select(projectionFn, { debounce: true }),
};

Selectors based on other selectors:

{
  immediate$: this.select(
    ...selectors.map((selector) => selector.immediate$),
    projectionFn
  ),
  debounced$: this.select(
    ...selectors.map((selector) => selector.debounced$),
    projectionFn,
    { debounce: true }
  ),
};

Considering we can memoize projectionFn once for both selectors returned this solution has many benefits:

  1. No need anymore to think if debounce can be passed and what effect will it have on other selectors and effects.
  2. Debounce performance boost to any selector used by a component
  3. Easy effects access to immediate store value
  4. Due to memoization, projectionFn will always evaluate once although there are two selectors.
  5. Due to select internal shareReplay using refCount if you don't use any of the selectors in the object there will be no ghost subscriptions.

The only thing lost here is the ability to pass any observable to select. This can be solved anyway. One option is having this functionality as a new method called for example createSelector and retaining the select functionality. This will also make this change non breaking.

Describe any alternatives/workarounds you're currently using

  1. Carefully use debounce as it is now - from my experience it can lead to a lot of weird results especially when combining debounced and non debounced selectors.
  2. Don't use debounce at all and pay the performance penalty (and actually the memoization penalty when using async pipe inside ngIf due to refCount).
  3. Wrap component store by yourself with this functionality

I would be willing to submit a PR to fix this issue

  • Yes
  • No

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions