I have two observables of arrays (settings, mappings). I need to generate from these an object[] array, where each object is of the form { setting, mapping } in order.
I've attempted to forkJoin the two observables, but I'm not sure how to proceed. The method is in a service and should return Observable<object[]>.
I was thinking I need to somehow zip and map the result of forkJoin to the objects, but I can't seem to get this done.
RXJS docs are not particularly helpful, or perhaps I don't know how to read them correctly. Any suggestions are appreciated in advance.
Update
I was able to get it to work with the following implementation:
return forkJoin([settings, mappings]).pipe(
map(([settings, mappings]) => {
const cards = settings.map((setting, index) => {
const card: Card = {
settings: setting,
apps: mappings[index].appInformation,
};
return card;
});
return cards;
})
);
I am too new to web programming to analyze the efficiency of my implementation. If there is a better way... please...
I was thinking I need to somehow zip and map the result
Not right but also not wrong. Zipping observables zips emisionwise, while you only get one emision with an array.
You have 2 choices:
You transform each observable to an observable emitting each array element and zip the transformed observables.
import { from, Observable, of, switchMap, zip } from 'rxjs'
const emitArrayOneTime1: Observable<number[]> = of([1, 2, 3, 4, 5]);
const emitArrayOneTime2: Observable<string[]> = of(['a', 'b', 'c', 'd', 'e']);
const emitArrayElements1 = emitArrayOneTime1.pipe(switchMap(arr => of(...arr)));
const emitArrayElements2 = emitArrayOneTime2.pipe(switchMap(arr => of(...arr)));
zip([emitArrayElements1, emitArrayElements2]).subscribe(console.log)
Or
You forkJoin both emits and zip the array results. In following exmaple I simplify the code by using lodash zip
import { forkJoin, Observable, of, map } from 'rxjs'
import { zip } from 'lodash';
const emitArrayOneTime1: Observable<number[]> = of([1, 2, 3, 4, 5]);
const emitArrayOneTime2: Observable<string[]> = of(['a', 'b', 'c', 'd', 'e']);
forkJoin([emitArrayOneTime1, emitArrayOneTime2]).pipe(
map(([arr1, arr2]) => zip(arr1, arr2))
).subscribe(console.log)
Related
I'm using RxJS 7, and I would like to have a child generator (another Observable) emitting values based on the parent data.
I was already able to achieve this, but the solution I found is not efficient in terms of CPU usage because it needs to build a new RxJS pipeline for every parent item, and I believe I'm not using here the full potential RxJS has.
Constraints:
Emitted values from the Parent needs to be available to child generator;
Parent needs to know when child flow is done;
Child Observable can have many operators;
Efficient!
The working example:
const { from, mergeMap, reduce, lastValueFrom } = rxjs
function run() {
const parentData = [{ parentId: 1 }, { parentId: 2 }, { parentId: 3 }]
from(parentData)
.pipe(mergeMap((parent) => lastValueFrom(getChildFlow(parent))))
.subscribe((parent) => console.log(parent))
}
function getChildFlow(parent) {
return from(childGenerator(parent))
.pipe(reduce((acc, value) => {
acc.inner.push(value)
return acc
}, { inner: [] }))
}
async function* childGenerator(parentData) {
for await (const index of [1, 2, 3]) {
yield { childId: index, ...parentData }
}
}
run()
<script src="https://unpkg.com/rxjs#^7/dist/bundles/rxjs.umd.min.js"></script>
The reason I'm looking for a more efficient implementation is because it's intended for a data intensive system which can have millions of items flowing.
Questions!
Does RxJS provide some operator to cover this scenario in a more efficient implementation? I really dug RxJS's documentation and didn't found anything, but I may have missed it.
Would it be possible to reuse the flow on the above implementation? The tricky part here is that the child generator needs to have the parent data.
PS: Don't mind the implementation details of the code above, it's just an example of what I'm trying to achieve, and doesn't cover all the precautions and additional steps I have to justify the use-case.
I found the solution to my problem.
It required using mergeMap, groupBy, reduce and zip.
I'm not convinced it's the best solution, so if you find another approach for this that you think is more efficient, I will certainly upvote your answer and mark it as correct answer over mine.
const { from, mergeMap, tap, zip, map, groupBy, reduce } = rxjs
function run() {
const parent$ = from([{ parentId: 1 }, { parentId: 2 }, { parentId: 3 }])
.pipe(tap(doWhatever))
const reducer = reduce(accumulator, [])
const child$ = parent$
.pipe(mergeMap(childGenerator))
.pipe(tap(doWhatever))
.pipe(groupBy((p) => p.parentId))
.pipe(mergeMap((group$) => group$.pipe(reducer)))
zip([parent$, child$])
.pipe(map((results) => ({ ...results[0], inner: results[1] })))
.pipe(tap(doWhatever))
.subscribe(console.log)
}
function accumulator(acc, cur) {
return [...acc, cur]
}
function doWhatever() {}
async function* childGenerator(parentData) {
for await (const index of [1, 2, 3]) {
yield { childId: index, ...parentData }
}
}
run()
<script src="https://unpkg.com/rxjs#^7/dist/bundles/rxjs.umd.min.js"></script>
This is more a logical problem then a RxJS problem, I guess, but I do not get it how to solve it.
[input 1]
From a cities stream, I will receive 1 or 2 objects (cities1 or cities2 are test fixtures).
1 object if their is only one language available, 2 objects for a city with both languages.
[input 2]
I do also have a selectedLanguage ("fr" or "nl")
[algo]
If the language of the object corresponds the selectedLanguage, I will pluck the city. This works for my RxJS when I receive 2 objects (cities2)
But since I also can receive 1 object, the filter is not the right thing to do
[question]
Should I check the cities stream FIRST if only one object exists and add another object. Or what are better RxJS/logical options?
const cities1 = [
{city: "LEUVEN", language: "nl"}
];
const cities2 = [
{city: "BRUSSEL", language: "nl"},
{city: "BRUXELLES", language: "fr"}
];
const selectedLang = "fr"
const source$ = from(cities1);
const result = source$.pipe(
mergeMap((city) => {
return of(selectedLang).pipe(
map(lang => {
return {
lang: city.language,
city: city.city,
selectedLang: lang
}
}),
filter(a => a.lang === selectedLang),
pluck('city')
)
}
)
);
result.subscribe(console.log)
If selectedLang is not an observable (i.e. you don't want this to change) then I think it would make it way easier if you keep it as a value:
const result = source$.pipe(
filter(city => city.language === selectedLang)
map(city => city.city)
);
There's nothing wrong from using external parameters, and it makes the stream easier to read.
Now, if selectedLang is an observable, and you want result to always give the city with that selectedLang, then you probably need to combine both streams, while keeping all the cities received so far:
const selectedLang$ = of(selectedLang); // This is actually a stream that can change value
const cities$ = source$.pipe(
scan((acc, city) => [...acc, city], [])
);
const result = combineLatest([selectedLang$, cities$]).pipe(
map(([selectedLang, cities]) => cities.find(city => city.language == selectedLang)),
filter(found => Boolean(found))
map(city => city.city)
)
Edit: note that this result will emit every time cities$ or selectedLang$ changes and one of the cities matches. If you don't want repeats, you can use the distinctUntilChanged() operator - Probably this could be optimised using an exhaustMap or something, but it makes it harder to read IMO.
Thanks for your repsonse. It's great value for me. Indeed I will forget about the selectedLang$ and pass it like a regular string. Problem 1 solved
I'll explain a bit more in detail my question. My observable$ cities$ in fact is a GET and will always return 1 or 2 two rows.
leuven:
[ { city: 'LEUVEN', language: 'nl', selectedLanguage: 'fr' } ]
brussel:
[
{ city: 'BRUSSEL', language: 'nl', selectedLanguage: 'fr' },
{ city: 'BRUXELLES', language: 'fr', selectedLanguage: 'fr' }
]
In case it returns two rows I will be able to filter out the right value
filter(city => city.language === selectedLang) => BRUXELLES when selectedLangue is "fr"
But in case I only receive one row, I should always return this city.
What is the best solution to this without using if statements? I've been trying to work with object destruct and scaning the array but the result is always one record.
// HTTP get
const leuven: City[] = [ {city: "LEUVEN", language: "nl"} ];
// same HTTP get
const brussel: City[] = [ {city: "BRUSSEL", language: "nl"},
{city: "BRUXELLES", language: "fr"}
];
mapp(of(brussel), "fr").subscribe(console.log);
function mapp(cities$: Observable<City[]>, selectedLanguage: string): Observable<any> {
return cities$.pipe(
map(cities => {
return cities.map(city => { return {...city, "selectedLanguage": selectedLanguage }}
)
}),
// scan((acc, value) => [...acc, { ...value, selectedLanguage} ])
)
}
This is basically what I am after. Just not sure the best way to return the combination of all three in the switchmap.
unsubscribes = combineLatest(
apiCall1,
apiCall12,
).pipe(
switchMap(([apiCall1Res, apiCall2Res]) => {
return apiCall3(apiCall1Res.Id)
})
).subscribe(([apiCall1Res, apiCall2Res, apiCall3Res]) => {
///Do work
})
If apiCall3 should after 1 and 2:
combineLatest(
apiCall1,
apiCall12,
).pipe(
switchMap(([apiCall1Res, apiCall2Res]) => {
return apiCall3(apiCall1Res.Id)
.pipe(map(apiCall3Res => [apiCall1Res, apiCall2Res, apiCall3Res]));
})
With ... you can save some space here:
combineLatest(
apiCall1,
apiCall12,
).pipe(
switchMap(results => apiCall3(apiCall1Res.Id)
.pipe(map(apicallResult3 => [...result, apicallResult3])
)
)
The sequence is correct, you just need to adjust how you treat the return values. When you use switchMap you transform the output of the observable sequence from the type you are receiving to the type of output of the observable you provide on the switchMap return. So you just must create an observable that returns the 3 values. You can do it by mapping the flow of the apiCall3 joining with the other two.
I propose one solution that can be adjusted to match your specific scenario if you need more. I created mock objects in order to make the sample directly executable for testing.
You can see the sample running with mock objects on the following stackblitz I created for you:
Editor: https://stackblitz.com/edit/rxjs-hjyrvn?devtoolsheight=33&file=index.ts
App: https://rxjs-hjyrvn.stackblitz.io
import { combineLatest, of, timer } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
// Mock objects...
const apiCall1 = timer(1).pipe(map(() => ({id: 1})));
const apiCall2 = timer(2).pipe(map(() => 2));
// apiCall3 mock created bellow on the fly...
let r1, r2; // <-- to save partial results because they are cutted from the flow bellow...
const source =
combineLatest(
apiCall1,
apiCall2,
).pipe(
tap(([apiCall1Res, apiCall2Res]) => { r1 = apiCall1Res; r2 = apiCall2Res;}),
map(([apiCall1Res, apiCall2Res]) => apiCall1Res.id), // adjust flow to apiCall3
switchMap((apiCall1ResId) => of(apiCall1ResId).pipe(map(id => id+2))), // <-- apiCall3 mock on the fly
map(apiCall3Res => [r1, r2, apiCall3Res])
);
source.subscribe(console.log);
As you can check on the output you receive the 3 values at the subscription observer code.
Here is an edited sample from learnrxjs. I want to filter the values in the type array. But thats not how it works: 'This condition will always return 'true' since the types 'string[]' and 'string' have no overlap.'
I am new to rxjs and cant figure out how to filter the array. Any advices? Is it possible?
const source = from([
{ name: 'Joe', age: 31, type: ['a', 'b'] },
{ name: 'Bob', age: 25, type: ['a'] }
]);
//filter out people with type b
const example = source.pipe(filter(person => person.type != 'a'));
//output: "People with type b: Bob"
const subscribe = example.subscribe(val => console.log(`Type a: ${val.name}`));
the filter() you are applying takes a function with signature T => boolean meaning that you will have to return a boolean true/false so it can filter out elements from the stream.
Your elements T are of type Object {name:string, age:number, type:array} so to filter on values in the type Array you will need to use the Array.indexOf prototype function:
source.pipe(filter(person => person.type.indexOf('b') == -1) // filter out people who have type b
I have a selector:
const mySelector = createSelector(
selectorA,
selectorB,
(a, b) => ({
field1: a.field1,
field2: b.field2
})
)
I know the selector is evaluated when any of its inputs change.
In my use case, I need to control "mySelector" by a third selector "controlSelector", in the way that:
if "controlSelector" is false, "mySelector" does not evaluate a new value even in the case "selectorA" and/or "selectorB" changes, and returns the memoized value
if "controlSelector" is true, "mySelector" behaves normally.
Any suggestions?
Selectors are pure functions..its will recalculate when the input arguments are changed.
For your case its better to have another state/object to store the previous iteration values.
You can pass that as selector and based on controlSelector value you can decide what you can return.
state : {
previousObj: {
...
}
}
const prevSelector = createSelector(
...,
(state) => state.previousObj
)
const controlSelector = createSelector(...);
const mySelector = createSelector(
controlSelector,
prevSelector,
selectorA,
selectorB,
(control, a, b) => {
if(control) {
return prevSelector.previousObj
} else {
return {
field1: a.field1,
field2: b.field2
};
}
}
)
Sorry for the delay...
I have finally solved the issue not using NGRX selectors to build up those "higher selectors" and creating a class with functions that use combineLatest, filter, map and starWith
getPendingTasks(): Observable<PendingTask[]> {
return combineLatest(
this.localStore$.select(fromUISelectors.getEnabled),
this.localStore$.select(fromUISelectors.getShowSchoolHeadMasterView),
this.memStore$.select(fromPendingTaskSelectors.getAll)).pipe(
filter(([enabled, shmView, tasks]) => enabled),
map(([enabled, shmView, tasks]) => {
console.log('getPendingTasks');
return tasks.filter(task => task.onlyForSchoolHeadMaster === shmView);
}),
startWith([])
);
}
Keeping the NGRX selectors simple and doing the heavy lifting (nothing of that in this example, though) in this kind of "selectors":
- will generate an initial default value (startWith)
- will not generate new value while filter condition fails (that is, when not enabled, any changes in the other observables do not fire a new value of this observable)