Unexpected issue during concatLatestFrom migration
Tags: angular, ngrx, rxjs
Published: 2023-07-12 12:10:48

---
title: Unexpected issue during concatLatestFrom migration
published: true
description: Migrate from RxJS withLatestFrom operator to NgRx concatLatestFrom operator with ease. Avoid silly issues by checking your codebase first and saving time following my pieces of advice.
tags: angular, ngrx, rxjs
---

When using NgRx as a state manager for Angular, you end up using `withLatestFrom` RxJS operator. It's often in effects to retrieve the current state from selectors.

NgRx v11 introduced  `concatLatestFrom` operator as a drop-in replacement for `withLatestFrom`. The migration should be seamless but sneaky issues can appear with `withLatestFrom` misuses.

## Identify **withLatestFrom** misuses

This RxJS operator shines when you need to retrieve an observable value (aka. selector) within an effect.

```ts
// Emits 0, 1, 2 and 3 with a 2s interval
selectCount$ = timer(0, 2_000).pipe(take(4));

effect$ = this.actions$.pipe(
  withLatestFrom(selectCount$),
  tap(([action, count]) => console.log(action, count))
);
```
In this example, the selector value is available in the effect. `action$` is the main observable while `selectCount$` is a "side observable" running separately. Having updates on the selector won't trigger the effect.

As soon as the effect is active, `withLatestFrom` side process kicks in watching for given observables changes. It keeps track of the last known value to make it synchronously available by the main observable.

> Curious about what "side process" means? Checkout [RxJS source code](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/withLatestFrom.ts#L79-L98), it does subscribe outside of the main observable scope

---

Let's look at another example but replace the selector with an HTTP request. The idea is to only trigger the request once and access its value.

```ts
// Emulates http request which don't emit immediately
httpRequest$ = of('result').pipe(delay(2_000));

effect$ = this.actions$.pipe(
  withLatestFrom(httpRequest$),
  tap(([action, result]) => console.log(action, result))
);
```

The above example works most of the time but can fail at some point. What happens if the effect receives an action before the request ends? 

Nothing, the operator acts as a filter and [ignores](https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/withLatestFrom.ts#L106-L110) the action. `withLatestFrom` can't provide the last value since the request didn't emit any value yet.

> Don't use `withLatestFrom` on observables without an initial value. Stick with selectors and be careful with other observables.

This issue is hard to spot since it doesn't happen often. `withLatestFrom` watch process starts at effect class instantiation time. Usually, there is enough time for the request to complete before the effect receives an action.

## What's the difference with **concatLatestFrom**?

`concatLatestFrom` is an RxJS operator provided by NgRx to address the effect's specific use cases. NgRx v15 adds an 
 [official ESLint rule](https://ngrx.io/guide/eslint-plugin/rules/prefer-concat-latest-from) which encourages migration. 

```ts
effect$ = this.actions$.pipe(
  withLatestFrom(selectItem(action.id)),
);
```

This snippet doesn't work. The intent is to retrieve an item from the store based on the incoming id. 

`withLatestFrom` can't help us in this case. The selector starts once at effect class instantiation time. At this moment, the effect didn't receive any action yet thus the item id isn't reachable.

```ts
effect$ = this.actions$.pipe(
  concatLatestFrom(action => selectItem(action.id)),
);
```

Did you notice the parameter changed from an observable to an  [arrow function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)? It's important to postpone the observable watch process.

The selector won't start at effect class instantiation time. Instead, the effect can start the selector on demand when it receives an action containing an id. It does so by executing the arrow function returning the selector to watch.

---

This example works fine, it looks safe to apply the new ESLint rule. Let's replace the existing `withLatestFrom` with its more flexible counterpart.

```ts
httpRequest$ = of('result').pipe(delay(2_000));

effect$ = this.actions$.pipe(
  concatLatestFrom(() => httpRequest$)
);
```

Unfortunately, this small change broke the effect. It seems `concatLatestFrom` ignores the action. Let's check this operator [source code](https://github.com/ngrx/platform/blob/master/modules/effects/src/concat_latest_from.ts#L56) to figure out what happens.

It leverages `withLatestFrom` which makes sense. We observed this operator might ignore actions under some conditions, right?

The request starts once the effect receives an action. At this point, the main observable tries to read the request value synchronously. It fails since the request didn't emit the response yet hence no "latest" value is available.

## Fix **combineLatestFrom** misuses

It's bad idea to use `LatestFrom` operators with observables without initial value (such as http requests). It means the observable to watch must emit without delay. 

Imagine an effect which needs to retrieve a [feature flag](https://martinfowler.com/articles/feature-toggles.html) from an API before running another request based on the action id.

```ts
effect$ = this.actions$.pipe(
  concatLatestFrom(() => featureFlagRequest$),
  switchMap(([action, flag]) => {
    if (!flag) {
       return legacyRequest(action.id);
    }
    return request(action.id);
  })
);
```

Thanks to your experience, you can spot the misuse of `concatLatestFrom` watching an HTTP request. 

Let's see how we can refactor this piece of code and keep the desired behaviour. The intuitive solution is to move the feature flag request into the `switchMap`.

```ts
effect$ = this.actions$.pipe(
  switchMap(([action, flag]) => featureFlagRequest$.pipe(
    switchMap(flag => {
      if (!flag) {
        return legacyRequest(action.id);
      }
      return request(action.id);
    })
  ))
);
```
Having both requests together adds nesting and result in complex RxJS code.

The request also runs on each action and not once slowing down the reactivity. It's good when having an up-to-date value is important but could have caching otherwise.

What about replacing `concatLatestFrom` with an observable composition operator? 

```ts
effect$ = this.actions$.pipe(
  switchMap(action => featureFlagRequest$.pipe(
    map(flag => [action, flag])
  ),
  switchMap(([action, flag]) => {
    if (!flag) {
       return legacyRequest(action.id);
    }
    return request(action.id);
  })
);
```
The feature flag request and the second request are in different code blocks. It looks like `concatLatestFrom` approach but `switchMap` waits for the request to complete.

The extra `map` operator is a downside. It could not scale with many requests. Another way to improve this is to leverage the `zip` operator.

```ts
effect$ = this.actions$.pipe(
  switchMap(action => zip(
    of(action),
    featureFlagRequest$, // Can safely add more requests here
  )),
  switchMap(([action, flag]) => {
    if (!flag) {
       return legacyRequest(action.id);
    }
    return request(action.id);
  })
);
```

The most scalable approach is quite verbose and complex. Prefer the previous solution for simpler use cases with a single request.

One last solution is to keep `withLatestFrom` for such cases while having its downsides in mind. Better avoid having both operators in your codebase to not confuse other developers.

## Wrapping up

Using `concatLatestForm` as suggested by ESLint rules is a nice idea. It helps when selectors needs to access action details but also ease mocking in unit tests.

Pay attention to the watched observables, it should emit without delay. If not, the operator ignores the action whereas it might work with `withLatestFrom`.

Watch-out for `withLatestFrom` misusages during the migration and consider using `switchMap` instead. Listening selectors is safe but other observables such as HTTP request are not. Check out my [Stackblitz playground](https://stackblitz.com/edit/concat-lastest-from-migration?file=src%2Fmain.ts) to perform your tests.

Thanks for reading!