combineAll does not emit on empty array
Asked Answered
L

4

6

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?

Letitialetizia answered 30/8, 2016 at 15:17 Comment(2)
This has been solved in github.com/ReactiveX/rxjs/issues/1910Parsnip
Note: There was no change to RxJS as a result of this issue - the resolution was just to point out that you should manually check for an empty array (same code included in accepted answer).Harman
L
4

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')
    }) 
Letitialetizia answered 1/9, 2016 at 8:27 Comment(0)
L
2

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.

Loella answered 31/8, 2016 at 7:33 Comment(1)
Thanks for the feedback! It's very true that it was never going to work with an empty array as input. A better solution (and accepted answer) was to bypass the combineAll/combineLatest entirely with a ternary selector and just return an Observable.of([]) if the length === 0.Parsnip
H
2

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());  
Harman answered 28/11, 2021 at 5:20 Comment(0)
C
1

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

combineLatest(potentiallyEmptyArray).pipe(
    startWith<any>([])
);
Churchill answered 2/9, 2021 at 13:31 Comment(2)
but then you get a glitch, rxjs.dev/api/operators/defaultIfEmpty is saferUnshod
.=// i agree \\=.Churchill

© 2022 - 2024 — McMap. All rights reserved.