1

I have a form in a component which should do two things when its values change:

  • update the view immediately
  • emit the form values to the parent after a short delay

A service in the parent component is going to send a new request to an API on form value change. This should not happen on every keystroke, to reduce requests. So I throttled emitting the event.

Unfortunately I have two subscribers now: One that handles the view update and a second with debounceTime that takes care of the EventEmitter:

private subscribeToChanges(): void {
  this.form.valueChanges.pipe(
    distinctUntilChanged(),
    takeUntil(this.isDestroyed$)
  ).subscribe(() => {
    this.updateView();
  });

  this.form.valueChanges.pipe(
    debounceTime(400),
    distinctUntilChanged(),
    takeUntil(this.isDestroyed$)
  ).subscribe(() => {
    this.changed.emit(this.form.value);
  });
}

I could add a timeout myself in the first subscriber, but this doesn't feel good as well.

What is the correct way of doing this?

lampshade
  • 2,470
  • 3
  • 36
  • 74

2 Answers2

2

You could try to use the tap operator and apply the debounceTime after the tap. Try the following

private subscribeToChanges(): void {
  this.form.valueChanges.pipe(
    distinctUntilChanged(),
    takeUntil(this.isDestroyed$),
    tap(value => this.updateView()),
    debounceTime(400)
  ).subscribe(() => {
    this.changed.emit(this.form.value);
  });
}
ruth
  • 29,535
  • 4
  • 30
  • 57
  • Thank you very much. This is a neat solution and it also helped me to understand `tap()` better. – lampshade Jun 15 '20 at 09:27
  • 1
    @lampshade just keep in mind, that tap is intended for side-effects. Minimizing side-effects in general will lead to a more predictable and maintainable code. Just because it's the shortest way, does not always mean it's the best way. – Jonathan Stellwag Jun 15 '20 at 10:40
1

I believe no way is objectively "correct" unless it doesn't work. Here's another way.

private subscribeToChanges(): void {
  const subscription = this.form.valueChanges.pipe(
    distinctUntilChanged(),
    takeUntil(this.isDestroyed$)
  );

  subscription.subscribe(() => {
    this.updateView();
  });

  subscription.pipe(
    debounceTime(400),
  ).subscribe(() => {
    this.changed.emit(this.form.value);
  });
}
Kaustubh Badrike
  • 495
  • 1
  • 4
  • 18
  • 2
    Nice approach, without any in pipe side effect. This will lead to a more maintainable, predictable code. – Jonathan Stellwag Jun 15 '20 at 10:39
  • @JonathanStellwag thank you for the information on side effects. So do you think this approach makes more sense for the current situation? – lampshade Jun 15 '20 at 11:20
  • @lampshade imo: With and without side effects, the code will work. It always depends onto your project. In mid to big projects the amount of maintenance is getting higher and higher. There are lot's of strategies to avoid an exponential growth of effort you need to invest. Not using side effect is one you can (imo should) go. Pipes without side effects are more predictable (for devs that are not into the function), can be tested easier (inputs are known - no mocking needed), help you more reusing parts of it and so on. – Jonathan Stellwag Jun 15 '20 at 13:05
  • 1
    @lampshade in my current project I am following the following principle: "Every side-effect is a custom observable". Just in case I already have an observable that already does what I need, I re-use it for another side-effect. This way I also learned to be more aware about when I really need to create a side effect. Most time you only need a side effect if you call an api or you update your view. Many projects often subscribe at lots of places between, where no side effect is needed. – Jonathan Stellwag Jun 15 '20 at 13:08
  • 1
    @JonathanStellwag Thank you very much for your insights, This was really helpful to understand side effects better. – lampshade Jun 15 '20 at 13:54