RxJS: defaultIfEmpty
tldr;
RxJS pipelines are convenient ways to get work done in your applications. You can essentially tell the app the steps it needs to take, and it will go step by step through the pipeline. But what if one of the steps will potentially be empty, but you want the pipeline to continue? That’s where the defaultIfEmpty
operator can come in handy. I recently learned about it when I had a similar situation, and it worked perfectly for me.
Background
I was recently working on an application where you needed to be able to add, edit, and delete data. The work needed to be done in a certain order: deletions first, then additions or edits. This is straightforward as long as there are actions for each of the above types to be done: at least one add, one edit, and one delete. But what happens if one of those types doesn’t need to be done in a given pipeline?
This is the situation I found myself in. If the delete observable was empty, nothing happened. I needed the pipeline to continue, however, and move on to adds and edits. I started searching around and found the defaultIfEmpty
operator, which solved my problem for me.
defaultIfEmpty
The defaultIfEmpty
operator does exactly what it sounds like: it provides a default value in the pipeline if the preceding observable doesn’t emit a value. This ensures that the pipeline doesn’t get stuck, but instead outputs a value.
Let’s look at a couple examples. First we’ll look at an example pipeline where there is a type for adds, edits, and deletes.
const deletions = [of('delete 1')];
const additions = [of('add 1')];
const edits = [of('edit 1')];
zip(...deletions)
.pipe(
tap((deletionResult) => console.log(deletionResult)),
switchMap(() => zip(...additions)),
tap((additionResult) => console.log(additionResult)),
switchMap(() => zip(...edits)),
tap((editsResult) => console.log(editsResult))
)
.subscribe();
In the above example, this pipeline will execute as expected. The deletion observables will run and emit a value, then the additions, then the edits. The result of all three sections will be logged to the console. Easy enough, right? But what do we do if we have no deletions to run?
const deletions2 = [];
const additions2 = [of('add 2')];
const edits2 = [of('edit 2')];
zip(...deletions2)
.pipe(
tap((deletionResult) => console.log(deletionResult)),
switchMap(() => zip(...additions2)),
tap((additionResult) => console.log(additionResult)),
switchMap(() => zip(...edits2)),
tap((editsResult) => console.log(editsResult))
)
.subscribe();
In this example, nothing gets logged to the console. We never move past the first step, because nothing is ever emitted from the initial zip(...deletions2)
portion of the pipeline. But just because we don’t have anything to delete doesn’t mean we don’t want to continue with adding and editing the other pieces. This is where the defaultIfEmpty
operator comes in handy. Insert it after a given step in the observable pipeline that could potentially emit no data. Then the pipeline will continue. Continuing with the above example, we can change it to the following to get our pipeline working again.
zip(...deletions2)
.pipe(
defaultIfEmpty([]),
tap((deletionResult) => console.log(deletionResult)),
switchMap(() => zip(...additions2)),
tap((additionResult) => console.log(additionResult)),
switchMap(() => zip(...edits2)),
tap((editsResult) => console.log(editsResult))
)
.subscribe();
Now our pipeline continues and values are logged to the console. The defaultIfEmpty([])
operator essentially says if the preceding observable doesn’t emit a value, emit an empty array and then continue on to the next step. So, although we don’t have anything to delete, we can still do our adds and edits.
Let’s look at one last example, where we have deletions and edits to do, but no additions. Can you guess what will happen?
const deletions3 = [of('delete3')];
const additions3 = [];
const edits3 = [of('edit 3')];
zip(...deletions3)
.pipe(
tap((deletionResult) => console.log(deletionResult)),
switchMap(() => zip(...additions3)),
tap((additionResult) => console.log(additionResult)),
switchMap(() => zip(...edits3)),
tap((editsResult) => console.log(editsResult))
)
.subscribe();
If you guessed nothing happens at all, you’re close. But it does finish the delete step before stopping. So your edits never occur. To fix the issue, we just need to add another defaultIfEmpty
operator.
zip(...deletions3)
.pipe(
tap((deletionResult) => console.log(deletionResult)),
switchMap(() => zip(...additions3).pipe(defaultIfEmpty([]))),
tap((additionResult) => console.log(additionResult)),
switchMap(() => zip(...edits3)),
tap((editsResult) => console.log(editsResult))
)
.subscribe();
Note: You can add the
defaultIfEmpty
operator in the pipe for the additions like I have above, or you can add it directly after theswitchMap
; either way will work.
With this minor change to our pipeline, it now finishes as we expect. The deletions are made, followed by the edits.
Conclusion
I was initially stuck with figuring out how to make sure my pipeline continued, even if there was an empty step in the pipeline. But I should have known there was an operator to help me out. There are a lot of operators, and while it makes RxJS seem overwhelming at first it’s also very convenient for situations like this. When you get stuck, ask for help and read the docs. You’re likely to find someone who’s run into the same problem and can point you in the right direction.
Here’s a StackBlitz with the examples we looked at above
Top comments (0)