Marcus Rådell
Marcus Rådell

Reputation: 620

combineAll does not emit on empty array

JSBIN Sample

I have a changeable set of child components (POJO object) that each have its own state stream. Each time a user triggers addChild/removeChild/clearChildren, a new set of children state streams is emitted with #switchMap. So far so good! (And so amazed by RxJS!)

With Rx.Observable.from(arrayOfStateStreams).combineAll() I get a good result as long as the arrayOfStateStreams isn't an empty array.

Since this is a partial state that is combined(Latest) on a higher level, I need to get an empty array emitted or the global state tree will contain old state data that is no longer true!

I can emit some reserved token like ['EMPTY-ARRAY-PLACEHOLDER-TOKEN'], but that's just weird. A better way would be to always append one last stream into the array so the last index can be considered trash. Still confusing code and state though. Using [null] is not OK, since we could have a child state of 'null'.

Anyone who can solve this in a good way? Can't this be supported since there should be no other representation of an empty array after #combineAll?

Upvotes: 6

Views: 3823

Answers (4)

Simon_Weaver
Simon_Weaver

Reputation: 145920

Note: Similar issues exist with combineLatest() (the static version) which can be solved using defaultIfEmpty() - which works, but it screws up the typing of the output.

// array of Observables
const animals: Observable<{ species: 'dog' | 'cat' }>[] = [];  

// Type '{ species: "dog" | "cat"; }[]' is not assignable to type 'never[]'.
combineLatest(animals).pipe(defaultIfEmpty([]));  

In TypeScript you need to either know the type of the object or use <any>[] which means you then lose typing completely.

If you have a concrete type you can use one of these:

defaultIfEmpty<Animal[]>([])

defaultIfEmpty([] as Animal[])

I often don't have a concrete type for the return value of an observable. So I came up with an operator:

 export const emptyArrayIfEmpty = () => <T>(observable: Observable<T[]>) =>
                                        observable.pipe(defaultIfEmpty([] as T[]));

Then I can add the following and get out an empty array if animals === [] without losing any typing information:

combineLatest(animals).pipe(emptyArrayIfEmpty());  

Upvotes: 2

Marcus R&#229;dell
Marcus R&#229;dell

Reputation: 620

Credits go to github user trxcllnt who provided the following answer:

combineAll won't emit unless the combined Observables emit at least one value, but you could check to ensure the collection you're combining is empty or not, and either combine or emit an empty Array:

 var arrayOfStreamsStream = Rx.Observable
    .of(
        [], [
            Rx.Observable.of('blah-1'), // component state.
            Rx.Observable.of('blah-2'),
            Rx.Observable.of('blah-3')
        ], [], [
            Rx.Observable.of('foo-1'),
            Rx.Observable.of('qux-2')
        ]
    )
    .switchMap(function onMap(coll) {
        return coll.length === 0 ?
            Observable.of(coll) :
            Observable.combineLatest(...coll);
    })
    .subscribe(function onSubscribe(data) {
        console.log('onSubscribe START')
        console.dir(data)
        console.log('onSubscribe END')
    }) 

Upvotes: 4

Martin Cremer
Martin Cremer

Reputation: 5572

a possible workaround is to just pipe it with startWith();

combineLatest(potentiallyEmptyArray).pipe(
    startWith<any>([])
);

Upvotes: 1

Nypan
Nypan

Reputation: 7246

This has nothing to do with combineAll. The problem is that Observable.from results in nothing (an empty observable) when passed an empty array.

The only viable solution that I can think of if you have to get a result from an empty array is to return something else in that case.

Ann example to illustrate the problem and a possible solution.

var data = [1, 2, 3, 4, 5];

log('With data: ');
Rx.Observable.from(data)
    .subscribe(function (d) { log('data: ' + d); });

// Prints: 
// With data: 
// data: 1
// data: 2
// data: 3
// data: 4
// data: 5

var data = [];

log('Without data: ');
var nullDataObject = { msg: 'my null data object' };
Rx.Observable.from(data.length == 0 ? [nullDataObject] : data)
    .subscribe(function (d) { log('data: ' + d); });

// Prints: 
// With data: 
// data: [object Object]

Runnable example on jsfiddle.

When consuming this you simply filter away the object representing an empty array where appropriate.

Upvotes: 2

Related Questions