Dispatch NgRx actions only when some properties changes on Rxjs Polling

Viewed 299

I have a Rxjs Polling effect like so :

updateProductData$ = createEffect(() =>
  this.actions$.pipe(
    ofType(fromActions.loadProduct),
    switchMap(_) =>
      this.http.get('endpoint').pipe(
        delay(1000),
        repeat(),
        switchMap((data) => [
          fromActions.updateFoo({foo: data.foo}),
          fromActions.updateBar({bar: data.bar}),
        ])
      )
    )
  );

How can I dispatch updateFoo and updateBar only when data.foo or data.bar change respectively?

I can improve this by using distinctUntilChanged, doing so the actions wont trigger if data.stuff changes, however, both actions still dispatch when either one changes.

...
     repeat(),
     distinctUntileChanged((prev, curr) => prev.foo === curr.foo && prev.bar === curr.bar) // works but it fires both actions when either one changes
     switchMap((data) => [
       fromActions.updateFoo({foo: data.foo}),
       fromActions.updateBar({bar: data.bar}),
     ])

I want to dispatch updateFoo when data.foo changes and updateBar when data.bar changes, knowing that data has a lot of other properties that can change as time goes on.

2 Answers

I think this could be an approach:

updateProductData$ = createEffect(() =>
  this.actions$.pipe(
    ofType(fromActions.loadProduct),
    switchMap(_) =>
      this.http.get('endpoint').pipe(
        delay(1000),
        repeat(),
        
        multicast(
          new Subject(),
          source => merge(
            // concerning `foo`
            source.pipe(
              distinctUntilChanged((prev, crt) => prev.foo === crt.foo),
              map(data => fromActions.updateFoo({foo: data.foo})),
            ),

            // concerning `bar`
            source.pipe(
              distinctUntilChanged((prev, crt) => prev.bar === crt.bar),
              map(data => fromActions.updateBar({bar: data.bar})),
            ),
          )
        )
      )
    )
  );

The source from multicast's second argument is the instance of the Subject which has been declared as the first argument. By using multicast, we can divide the problem into 2 other smaller problems, without redundantly subscribing to the source(that's why a Subject has been used).

You could try to use pairwise operator to get the previous state and explicitly check for the previous value. I've used startWith(null) to trigger the emission for the first value from the HTTP request. Without it, it won't start emitting until the second call

stop$ = new Subject<any>();
poll$ = timer(0, 1000).pipe(
  startWith(null),          // <-- emit `[null, data]` for first emission
  switchMap(this.http.get('endpoint')),
  pairwise(),
  map((data) => ({
    foo: {prev: data[0].foo, current: data[1].foo},
    bar: {prev: data[0].bar, current: data[1].bar}
  })),
  takeUntil(stop$)
);

updateProductData$ = createEffect(() =>
  this.actions$.pipe(
    ofType(fromActions.loadProduct),
    switchMap(_ =>
      poll$.pipe(
        switchMap((data) => {
          let result = [];

          if (data.foo.prev && data.foo.prev !== data.foo.current) 
            result.push(fromActions.updateFoo({foo: data.foo}))
          if (data.bar.prev && data.bar.prev !== data.bar.current) 
            result.push(fromActions.updateBar({bar: data.bar}))

          return result;
        })
      )
  )
);

Note: I've not used NgRX and this is untested code.

Related