Sharpen your NgRx effects

with custom RxJS operators

NgRx effects are written by manipulating an action stream with RxJS operators. The NgRx team created a ofType operator that allows us to filter out actions based on their type(s). Some other operators allow you to map some action(s) to a follow-up action. We can use filter to stop an effect from executing when some condition is not met. There are dozens of possibilities and even more operators to chose from. At some point, your code might look something like this.

// Some constants
export const MINIMUM_OIL_LEVEL = 10;

// Actions
export const startCar = createAction('[Car] Start car', props<{
  carId: string;
  oilLevel: number;
  // ...
}>());

export const startCarFailed = createAction('[Car] Start car failed');

// Effects
carIsAllowedToStart$ = createEffect(() => this.actions$.pipe(
  ofType(startCar),
  filter(({car}) => car.oilLevel >= MINIMUM_OIL_LEVEL),
  // START THE CAR!
));

For the code, I used NgRx actions, an NgRx effect, object destructuring and RxJS operators. I added the links so you can read up if you need to.

In the example, our application does not allow a car to start when the oil level is below a certain minimum. Now consider that for a complex object like a car, you have a lot more variables to take into account. You might need to check whether the battery has sufficient power, whether there is enough fuel, etc. Soon enough, your code will look something like this.

carIsAllowedToStart$ = createEffect(() => this.actions$.pipe(
  ofType(startCar),
  filter(({car}) => car.oilLevel >= MINIMUM_OIL_LEVEL
    && car.fuelLevel >= MINIMUM_FUEL_LEVEL
    && car.batteryLifeSufficient && car.keyIsInside
    && car.randomParameter !== '34'
    // The list goes on and on
  ),
  // START THE CAR!
));

I added some extra parameters like fuel level, battery life, whether the key is inside and other (random) parameters. This is code with a lot of symbols and so the text becomes hard to read. You can assume for a very complex object that the list goes on.

The solution: custom RxJS operators

There are so many resources on creating custom RxJS operators. I bet you can find even more on Google! All the credits for my code below go to the people that wrote those other articles, but I based most of it on netbasal's article.

export function whenThereIsSufficientOil() {
  return function <T extends {oilLevel: number}>(source: Observable<T>): Observable<T> {
    return new Observable((subscriber: Subscriber<T>) => {
      return source.subscribe({
        next(value) {
          if (value.oilLevel >= MINIMUM_OIL_LEVEL) {
            subscriber.next(value);
          }
        },
        error(error) {
          subscriber.error(error);
        },
        complete() {
          subscriber.complete();
        }
      });
    });
  };
}

We can create operators like this for all our conditions. This looks like a lot of extra code, but you could refactor that if you want to.

type CarType = {
  oilLevel: number; 
  batteryLifeSufficient: boolean;
  // More properties :-)
};

function checkCar(condition: (value: CarType) => boolean) {
  return function <T extends CarType>(source: Observable<T>): Observable<T> {
    return new Observable((subscriber: Subscriber<T>) => {
      return source.subscribe({
        next(value) {
          if (condition(value)) {
            subscriber.next(value);
          }
        },
        error(error) {
          subscriber.error(error);
        },
        complete() {
          subscriber.complete();
        }
      });
    });
  };
}

export function whenThereIsSufficientOil() {
  return checkCar(car => car.oilLevel >= MINIMUM_OIL_LEVEL);
}

export function whenTheBatteryLifeIsSufficient() {
  return checkCar(car => car.batteryLifeSufficient);
}

export function whenThereIsEnoughFuel() {
  return checkCar(car => car.fuelLevel >= MINIMUM_FUEL_LEVEL);
}

export function whenTheKeyIsInside() {
  return checkCar(car => car.keyIsInside);
}

export function whenTheRandomParameterIsValid() {
  return checkCar(car => car.randomParameter !== '34');
}

The result: a readable NgRx effect

With all those custom operators created, it's about time we use them. Combining these with the effect code we have will look something like this:

carIsAllowedToStart$ = createEffect(() => this.actions$.pipe(
  ofType(startCar),
  whenThereIsSufficientOil(),
  whenThereIsEnoughFuel(),
  whenTheBatteryLifeIsSufficient(),
  whenTheKeyIsInside(),
  whenTheRandomParameterIsValid(),
  // The list goes on and on
  ),
  // START THE CAR!
));

Now the code is so much easier to read. You can unit test the operators separately with a library like RxJS marbles to cover all edge cases.

Conclusion

A complex NgRx effect with large filter functions can become hard to read, especially when multiple symbols are used together with multiple types of casing (snake case, camel case). Using custom RxJS operators, you can clean up your effect and take that complexity away of your effects file. What you end up with is code that is more testable, more readable and so by definition easier to maintain.

I'm not saying you should always do this. I've seen many projects where the effects contain one or zero filters. Making this kind of change depends on the situation. Maybe you can use it or maybe the effort outweighs the value for your project. So, what are your thoughts?