How to make Observable.flatMap to wait to resolve - javascript

Playing with RxJS and React, I'm having problem of how to wait for data in Observable.fromPromise generated within map on another Observable.
I have async helper:
const dataStreamGenerator = (url = CLIENTS_DATA_URL) => (
Rx.Observable.fromPromise(fetch(url))
.flatMap(response => Rx.Observable.fromPromise(response.json()))
.catch(err => Rx.Observable.of(new Error(err)))
);
Then I have actions.fetchClients$ which is Rx.Subject:
actions.fetchClients$.map((url = CLIENTS_DATA_URL) => {
const ts = Date.now();
console.log('FETCH CLIENTS with: ', url);
return dataStreamGenerator(url);
}).map(val => {
console.log('GOT DATA IN REDUCER: ', val);
const error = (val instanceof Error) ? val.message : undefined;
const data = error ? undefined : val;
actions.receivedClientsData$.next({ data, error, ts: Date.now() });
return (state) => state;
})
(Yes, trying to mimick Redux in RxJS).
Whan I test the dataStreamGenerator, it works ok (with ava) and delivers data:
test('dataStreamGenerator', t => {
const ds$ = dataStreamGenerator(CLIENTS_DATA_URL);
return ds$.do((data) => {
t.is(data.length, 10);
});
});
(AVA automatically subscribe to observable and consume it, so there is no need to subscribe).
But the actions.fetchClients$.map((url = CLI... second map (starting... console.log('GOT DATA IN REDUCER: ', val);... is still getting the Observable and not the data from dataStream$.
I tried all possible combinations of map and flatMap in fetchClients$ code but still no luck.
My test code is:
test.only('fetchClients$', t => {
const initialState = {};
actions.fetchClients$.next('http://jsonplaceholder.typicode.com/users');
reducer$.subscribe(val => {
console.log('TEST SUBSCRIBE VAL: ', val);
t.pass();
});
actions.fetchClients$.next('http://jsonplaceholder.typicode.com/users');
});
I cant figure out how to wait to the Observable dataStreamGenerator(url); to emit data and not the Observable.
Thanks.

You need to flatten the results of what you returned from dataStreamGenerator.
actions.fetchClients$
//This is now a flatMap
.flatMap((url = CLIENTS_DATA_URL) => {
const ts = Date.now();
console.log('FETCH CLIENTS with: ', url);
return dataStreamGenerator(url);
});
The map operator delivers the value as-is down stream, whereas flatMap will flatten Observables, Arrays and Promises such that their values are what get propagated.
It works in the test case because you are directly subscribing to the Observable returned by dataStreamGenerator.

Related

Batching React updates across microtasks?

I have code that looks something like:
// File1
async function fetchData() {
const data = await fetch(...);
setState({ data });
return data;
}
// File2
useEffect(() => {
(async () => {
const data = await fetchData();
setState({ data });
})();
});
This triggers 2 React commits in 1 task. This makes my app less than 60FPS. Ideally, I'd like to batch the 2 setStates. Currently, it looks like this:
Pink represents React commits (DOM operations). The browser doesn't have a chance to repaint until the second commit is done. I can give the browser a chance to repaint by adding await new Promise(succ => setTimeout(succ, 0)); between the setStates, but it'll be better if I could batch the commits.
It's also pretty much impossible to refactor this, since the useState exists in separate files.
I tried unstable_batchedUpdates but it doesn't work with async.
You can group fetchData, when fetchData is called with the same argument the cache is checked for a promise and that promise is returned instead of creating a new one (make a new fetch).
When the promise resolves then that cache entry is removed so when component mounts again it will fetch again. To change this behaviour you can pass a different cache object to the group funciton.
//group function (will always return promise)
const createGroup = (cache) => (
fn,
getKey = (...x) => JSON.stringify(x)
) => (...args) => {
const key = getKey(args);
let result = cache.get(key);
if (result) {
return result;
}
//no cache
result = Promise.resolve(fn.apply(null, args)).then(
(r) => {
cache.resolved(key); //tell cache promise is done
return r;
},
(e) => {
cache.resolve(key); //tell cache promise is done
return Promise.reject(e);
}
);
cache.set(key, result);
return result;
};
//cache that removes cache entry after resolve
const createCache = (cache = new Map()) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
//remove cache key when resolved
resolved: (key) => cache.delete(key),
//to keep cache:
//resolved: () => 'NO_OP',
};
};
//fetch data function
const fetchData = (...args) => {
console.log('fetch data called with', args);
return new Promise((resolve) =>
setTimeout(() => resolve(args), 1000)
);
};
//grouped fetch data
const groupedFetchData = createGroup(createCache())(
fetchData
);
groupedFetchData(1, 2, 3).then((resolve) =>
console.log('resolved with:', resolve)
);
groupedFetchData(1, 2, 3).then((resolve) =>
console.log('resolved with:', resolve)
);
I think you should be able to so something along the lines of this, the aim being to cache the calls for a certain amount of time and then pass them all to unstable_batchedUpdates at once.
import { unstable_batchedUpdates } from 'reactDOM'
import raf from 'raf'
const cache = []
let rafId = null
function setBatchedState(setState, data) {
cache.push({ setState, data })
if(!rafId) {
rafId = raf(() => {
unstable_batchedUpdates(() => {
cache.forEach(({setState, data}) => setState(data))
})
rafId = null
cache = []
})
}
}
export default setBatchedState
This is using requestAnimationFrame to debounce the calls to unstable_batchedUpdates, you may prefer to use setTimeout depending on your use case.

Cloud Functions .onCall() unable to return data

I have merged three Observables via pipe() and then I'm converting it to a Promise so I can return it. I have tested the call with onRequest() and it works just fine. However with onCall() I keep getting this error:
Error Domain=NSCocoaErrorDomain Code=3840 "JSON text did not start with array or object and option to allow fragments not set." UserInfo={NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set.}
I have tried a lot of things but all of them return the same error. Am I doing something wrong here?
export const getNotifications = functions.region('europe-west1').https.onCall((data, context) => {
const notificationsObs = rxjs.from(db.collection('notifications').where('receiverID', '==', 'jmwJLofoKpaQ4jligJKPc24UEe72').get());
const requestObs = (requestsRef: any) => rxjs.from(db.getAll(...requestsRef));
const packageObs = (packagesRef: any) => rxjs.from(db.getAll(...packagesRef));
const notifications: Notification[] = [];
return notificationsObs.pipe(
mergeMap(snapshot => {
//
return requestObs(requestsRef);
}),
mergeMap((requests) => { // kthen requests
//
return packageObs(packagesRef)
})
).toPromise().then(() => {
return { notifications: "test" };
}).catch(err => {err})
});
Swift
functions.httpsCallable("getNotifications").call() { (result, error) in
if let error = error as NSError? {
print(error)
}
print(result?.data) // nil
}
So somehow onCall() is not supported in europe-west1. I removed the region and it works fine.

How to modify useState array one by one in mapped promise

I have
const [items, setItems] = useState<any[]>(itemsB.map(() => ({ loading: true })));
and
itemsB.map(async (itemB: any, index: number) => {
searchItems(itemB).then(result => {
const newItems = [...items]; // Ref A
newItems[index] = result;
setItems(newItems);
})
})
Because the inner function is an async fetch, items come back in an unpredictable order, and I want to add them to items as soon as they're ready, changing the placeholder loading object with the actual result. This almost does that, but the items referenced at Ref A does not update, so it cycles through changing each thing and then at the end only the last item to have been retrieved appears.
If I do a Promise.all().then(), it waits until all items are retrieved to execute the then part, so I'm wondering how to set items as they are resolved.
The calls made to setItems are batched to optimize performance. If the previous state relies on the current state, use the overloaded version of setItems which takes the previousState as the first argument.
searchItems(itemB).then(result => {
setItems(items => {
const newItems = [...items]; // Ref A
newItems[index] = result;
return newItems;
});
})
Maybe this would work:
itemsB.forEach(async (itemB: any) => {
searchItems(itemB).then(result => {
setItems(prevItems => [...prevItems, result]);
});
});
Note: this should not be in the render function. you should place that in componentDidMount/Update or useEffect hook.
Use can use the async library to help you to solve the problem as below:
import eachOf from 'async/eachOf';
// assuming items is an array of async items
async.eachOf(itemsB, async (itemB: any, index: number, callback: ReturnType<string | void>) => {
// Perform operation on itemB here.
console.log('Processing item ' + itemB);
try {
const result = await searchItems(itemB);
const newItems = [...items]; // Ref A
// Settting-up new items
newItems[index] = result;
setItems(newItems);
callback();
}catch(e) {
callback(e)
}
}, function(err) {
// If any of the item processing produced an error, err would equal that error
if( err ) {
// One of the iterations produced an error.
// All processing will now stop.
console.log('An item failed to process');
} else {
console.log('All items have been processed successfully');
}
});

React state in render is unavailable inside return

I have these methods that do some fetching, and then once done, they set the state. But the render is called before the state is done and does not update.
The below seems to work on it's own, but takes a minute to finish.
//returns an promise with Array
getTopIDs(url) {
return fetch(url).then(blob => blob.json()).then(json => json)
}
// makes a URL fetchs JSON and return promise with single ID
getStory(id) {
let url = `https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`
return fetch(url).then(blob => blob.json()).then(json => json)
}
// call above methods, set state when done
componentDidMount() { //
let arr = []
let promise = new Promise((resolve, reject) => {
let data = this.getTopIDs("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty").then((idArr) => {
idArr.forEach((id, index) => {
this.getStory(id).then(res => {
arr.push(res)
})
})
//resolve once all pushed to arr
resolve(arr)
})
})
// set state once array is completed
promise.then(res => {
return this.setState({data: arr})
})
}
Then in the render below it logs 'no', 'no' and stops. Trying it outside the return it logs 'no','yes'. Searching other posts for this I tried setting a boolean when done and using the state callback but those did not work (full disclosure: I don't really understand the setState callback option)
render() {
return (
<div>
{
this.state.data.length
? console.log('yes')
: console.log('no')
}
</div>)
}
I need render to handle this.state.data only when done. How can I do it?
Add fiddle: https://jsfiddle.net/drumgod/e2atysu3/6/
Your method this.getStory() is async but your handling of the array creation is sync inside your promise.
You need to either use async/await or only run your resolve(arr) after idArr.forEach() is for sure completed (which may be easier to do using Promise.all(idArr.map(...)) where the ... is returning the result from this.getStory()).
This is how you'll want to set your state inside getStory:
this.setState(prevState => ({
data: [...prevState.data, res]
}))
As mentioned in the comments, this would render the component for each data point in the forEach.
In order to avoid this issue, this is how componentDidMount() should be formatted:
componentDidMount() {
const arr = [];
this.getTopIDs("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty").then((idArr) => {
idArr.forEach((id, index) => this.getStory(id).then(res => arr.push(res)));
this.setState(prevState => ({ data: [...prevState.data, arr] }))
})
}
This also lets you get rid of the promise.then call at the end.

combining results from multiple rxjs observables

I have an autocomplete input which, as the user types, fetches data from multiple endpoints, such as:
//service call to fetch data and return as single observable
getAutocompleteSuggestions() {
const subs$ = [
this.http.get(endpoint1),
this.http.get(endpoint2),
this.http.get(endpoint3)
];
return Observable.forkJoin(...subs$);
}
Each of these endpoints returns data of the form:
{ data: [], status: xyz }
I would like to use switchmap as I want to only show results from the final call, and have tried the following:
this.getAutocompleteSuggestions(query)
.switchMap(res => {
return res.data;
})
.subscribe((results: any) => {
this.results = results;
});
But the 'res' in the switchmap is an array, any idea how results can contain a single array containing the data from the response of any number of observables?
I don't fully understand what you want, but I think this is it:
$filter: Subject<string> = new Subject(); //I guess we have some value to filter by??
Push a value to the subject:
this.$filter.next(myNewValue);
In the constructor or init:
this.$filter
.switchMap(filterValue => { //Get the values when filter changes
subs$ = [
this.http.get(endpoint1 + filterValue),
this.http.get(endpoint2 + filterValue),
this.http.get(endpoint3 + filterValue)
];
return Observable.forkJoin(...subs$);
})
.map(results => { //now map you array which contains the results
let finalResult = [];
results.forEach(result => {
finalResult = finalResult.concat(result.data)
})
return final;
})
.subscribe(); //Do with it what you want
The entire steam will be executed again when we put a new value into our subject. SwitchMap will cancel all ready requests if there are any.
I have used something similar to this, and worked well so far:
let endpoint1 = this.http.get(endpoint1);
let endpoint2 = this.http.get(endpoint2);
let endpoint3 = this.http.get(endpoint3);
forkJoin([endpoint1, endpoint2, endpoint3])
.subscribe(
results => {
const endpoint1Result = results[0].map(data => data);
const endpoint2Result = results[1].map(data => data);
const endpoint3Result = results[2].map(data => data);
this.results = [...endpoint1Result, ...endpoint2Result, ...endpoint3Result];
},
error => {
console.error(error);
}
);
Obviously is a pretty simple example, you'll be able to handle better the results to suit your needs.

Categories

Resources