I've poked about SO and found many similar questions/answers but I may be missing something in my basic understanding on how to work with with this stack.
I'm working on a react native project along with RXJS/obervables. At some point I doing file downloads, this part is not a problem. A combo of pre-existing axios-rxjs and react-native-file-system get me where I want. The issue is I'm not sure how to handle it cleanly without async/await which I understand is an anti-pattern.
I want to transform this working code into a clean obervable-style flow.
I've implemented an Epic that does the operations I want as such:
const myDownloadEpic = (
action$,
state$
) =>
action$.pipe(
ofType(myDownloadActionType), // catches the relevant action
map(action => action.payload),
mergeMap(payload =>
downloadManager // a class with the relevant utils to get files
.download(payload), // this axios call returns my Blob file as Observable<Blob>
.pipe(
mergeMap(async response => {
// a promise is returned by RNFS to read the contents of a folder
const files = await RNFS.readDir(RELEVANT_TARGET_PATH)
...
// a promise returned from a custom function that converts my blob to bas64
const data = await convertToBase64(response)
// another promise returned by RNFS to write to the file system
await RNFS.writeFile(FULL_PATH, data, 'base64');
...
})
)
)
)
I've tried splitting this into several pipes, for example, I tried splitting the READ operation into a previous pipe but it ends up looking very verbose. Is there not a clean simple way to "hold" until the promises are done so I can make decisions based on their result?
What would be considered cleaner in this situation?
You can try something like this. It should be roughly equivalent to what you've written above.
const myDownloadEpic = (
action$,
state$
) => action$.pipe(
ofType(myDownloadActionType),
map(action => action.payload),
mergeMap(payload => downloadManager.download(payload)),
mergeMap(response => from(RNFS.readDir(RELEVANT_TARGET_PATH)).pipe(
map(files => ({response, files}))
)),
mergeMap(values => from(convertToBase64(values.response)).pipe(
map(data => ({...values, data}))
)),
mergeMap(({response, files, data}) => RNFS.writeFile(FULL_PATH, data, 'base64'))
);
The from() operator can convert a promise into an observable that will emit the promised value then completes.
If you need to wait until all promises are resolved, I recommend forkJoin() as it won't emit a value until all observables complete.
Lastly, to make the code a little cleaner, I would also recommend declaring separate variables/functions to define your observables for each promise.
const files$ = from(RNFS.readDir(RELEVANT_TARGET_PATH));
const getData = (response: unknown) => from(convertToBase64(response)).pipe(
mergeMap(data=>
from(RNFS.writeFile(FULL_PATH, data, 'base64')).pipe(
mapTo(data)
)
)
);
const myDownloadEpic = (action$, state$) =>
action$.pipe(
ofType(myDownloadActionType),
map(({ payload }) => payload),
mergeMap(payload => downloadManager.download(payload)),
mergeMap(response =>
forkJoin({
files: files$,
data: getData(response)
})
),
map(({ files, data }) => {
// check something with `files` and `data`
})
);
I'm assuming RNFS.writeFile() response is void, so I put it as an effect when subscribing to getData(). The mapTo() operator ignores any emitted value from the source observable, and returns whatever value you put in the parameter.
Related
I am watching videos to learn MongoDB Express.js VueJS Node.js (MEVN) stack.
And I want to create a seed directory and also use promise functions
// const delay = require('delay')
const Promise = require('bluebird')
const songs = require('./songs.json')
const users = require('./users.json')
const bookmarks = require('./bookmarks.json')
const historys = require('./history.json')
sequelize.sync({ force: true })
.then( async function () {
await Promise.all(
users.map( user => {
User.create(user)
})
)
await Promise.all(
songs.map( song => {
Song.create(song)
})
)
//I have to add this line
// ---> await delay(1000)
await Promise.all(
bookmarks.map( bookmark => {
Bookmark.create(bookmark)
})
)
await Promise.all(
historys.map( history => {
History.create(history)
})
)
})
I have four tables with seeds to create, and the last two tables data must be created after the former two tables data. (They are foreign keys)
But every time I run this file, the last two tables data will be created first
The only way I can prevent this is to add delay(1000) between them.
I am wondering if there exists any efficient way to solve this issue~
Thank you.
Race conditions like this one is always caused by that promises weren't properly chained.
A promise should be returned from map callback:
await Promise.all(
users.map( user => User.create(user))
);
etc.
Not returning a value from map is virtually always a mistake. It can be prevented by using array-callback-return ESLint rule.
If User.create(user), etc. were Bluebird promises with default configuration, not chaining them would also result in this warning.
My assumption why your code might fail:
You're not returning the Promises that I guess /(User|Song|Bookmark|History).create/g return to the Promise.all() function, since your map callback is not returning anything.
If you're using Arrow functions with brackets, then you need to explicitly specify the return value (using the familar return keyword).
Otherwise you can just omit the curly brackets.
My suggestion is, that you refactor you're code by utilizing Promise .then()-Chaining.
For you're example, I would suggest something like this:
const Promise = require('bluebird')
const songs = require('./songs.json')
const users = require('./users.json')
const bookmarks = require('./bookmarks.json')
const histories = require('./history.json')
sequelize.sync({
force: true
}).then(() =>
Promise.all(
users.map(user =>
User.create(user)
)
).then(() =>
Promise.all(
songs.map(song =>
Song.create(song)
)
)
).then(() =>
Promise.all(
bookmarks.map(bookmark =>
Bookmark.create(bookmark)
)
)
).then(() =>
Promise.all(
histories.map(history =>
History.create(history)
)
)
)
);
Context
I have a lot of actions of the form: LOAD_XYZ,LOAD_XYZ_FAIL, LOAD_XYZ_SUCCESS for different page elements that need to be loaded.
I frequently wish to perform a redirect (react-router) after some item gets loaded, but since redux-observable does not return a promise I cannot perform the redirect in my component - but instead I have to do it using redux and push('...') from react-router-redux.
That unfortunately leads to a lot of duplication as I have LOAD_XYZ and LOAD_XYZ_REDIRECT_TO versions of the same action, plus two epics for each.
This is starting to feel wasteful and with too much redundancy
const fetchUserEpic = action$ =>
action$.ofType(FETCH_USER)
.mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`)
.map(response => fetchUserFulfilled(response))
);
const fetchUserAndRedirectEpic = action$ =>
action$.ofType(FETCH_USER_AND_REDIRECT)
.mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`)
.concatMap(response => [fetchUserFulfilled(response), push(action.redirectTo)])
);
Problem
Is there some sort of pattern/approach where I can sequence actions to be dispatched after some kind of async request has completed, to avoid redundancy and having to implement multiple versions of the same epic.
For example, I would like instead to have a separate REDIRECT_TO action that I can sequence after the item was loaded.
Something like these (imaginary) solutions:
dispatch(fetchUser(...)).then(redirectTo("..."))
dispatchSequence(fetchUser(...), redirectTo("..."))
I know redux-thunk can do this but then I miss out on all the rxjs operators.
I'm probably trenching into dangerous grounds but I did find a way to do this.
First we make a middleware which will add a completion observable as metadata to actions.
import { Subject } from "rxjs";
export const AddListenerMiddleware = store => next => action => {
const forkedAction = Object.assign({}, action, { meta: { listener: new Subject()}});
next(forkedAction);
return forkedAction.meta.listener.asObservable();
};
Since this is a Subject we can call next on it to emit some value. Creating a notify helper allows us to notify any subscribers of when the action has completed (plus pass down the data).
// notify doesn't consume but just emits on the completion observable
const notify = (action, fn) => source =>
Observable.create(subscriber =>
source.subscribe(emmission => {
if (action.meta && action.meta.listener) {
action.meta.listener.next(fn(emmission));
}
subscriber.next(emmission);
})
);
const fetchUserEpic = action$ =>
action$.ofType(FETCH_USER)
.mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`)
.map(response => fetchUserFulfilled(response))
.pipe(notify(action, response => response.data))
);
And now our dispatch returns an observable on which we can do standard rxjs operations. So this finally this becomes possible:
dispatch(fetchUser(...))
.subscribe(({id}) => dispatch(redirectTo("/" + id)))
Disclaimer: Use at your own risk!
I am currently using an action creator to pass a callback function into an Epic, however I run into the common error: Actions must be plain JavaScript objects...
Is there a way of executing the callback and still proceeding with the observable inside an Epic? I've also tried using the curly braces and an explicit return to assign the chain to a variable and return it after invoking the callback, but I run into the same problem.
Code:
const selectItem = (item, activeRow, cb) => ({ type: SELECT_ITEM, activeRow, item, cb });
const selectItemEpic = action$ =>
action$.ofType(SELECT_ITEM)
.mergeMap(action =>
Observable.forkJoin(
ajax.getJSON(...),
ajax.getJSON(...)
)
.map(res => returnSelectedItem({ ...res[0].response, ...res[1].response }, action.activeRow))
.map(() => action.cb()) // failing here
.takeUntil(action$.ofType(SELECT_ITEM))
);
The map operator is meant as a 1:1 mapping--so it expects you to return something in your projection function. Right now you're returning what ever the result of action.cb() is. If this isn't an action (e.g. it returns nothing/undefined), that would be the source of your error.
It's not 100% clear what returnSelectedItems() returns, if this actually returns the action you want to emit from your epic, you could instead use the do operator to make the action.cb() side effect without affecting the actual values flowing through the stream.
const selectItemEpic = action$ =>
action$.ofType(SELECT_ITEM)
.mergeMap(action =>
Observable.forkJoin(
ajax.getJSON(...),
ajax.getJSON(...)
)
.map(res => returnSelectedItem({ ...res[0].response, ...res[1].response }, action.activeRow))
.do(() => action.cb()) // do, instead of map
.takeUntil(action$.ofType(SELECT_ITEM))
);
I'm looking for way of dispatching multiple redux actions in a single Epic of redux-observable middleware.
Let's assume I have following Epic. Everytime when SEARCH event happens, Epic loads data from backend and dispatches RESULTS_LOADED action.
searchEpic = (action$) =>
action$
.ofType('SEARCH')
.mergeMap(
Observable
.fromPromise(searchPromise)
.map((data) => {
return {
type: 'RESULTS_LOADED',
results: data
}
})
)
Now, let's assume that I need dispatch additional action when the searchPromise is resolved.
The simplest way of doing so seems to have a second epic that will listen to RESULTS_LOADED and dispatch the second action. Like so:
resultsLoadedEpic = (action$) =>
action$
.ofType('RESULTS_LOADED')
.map(({results} => {
return {
type: 'MY_OTHER_ACTION',
results
}
})
In this simple example it's quite easy. But when Epics grow, I tend to find myself having a lot of redux actions which sole purpose is to trigger other actions. Additionally, some of the rxjs code needs to be repeated. I find this a bit ugly.
So, my question: Is there a way to dispatch multiple redux actions in a single Epic?
There is no requirement that you make a one-to-one in/out ratio. So you can emit multiple actions using mergeMap (aka flatMap) if you need to:
const loaded = (results) => ({type: 'RESULTS_LOADED', results});
const otherAction = (results) => ({type: 'MY_OTHER_ACTION', results});
searchEpic = (action$) =>
action$
.ofType('SEARCH')
.mergeMap(
Observable
.fromPromise(searchPromise)
// Flattens this into two events on every search
.mergeMap((data) => Observable.of(
loaded(data),
otherAction(data))
))
)
Note that any Rx operator that accepts an Observable also can accept a Promise, Array, or Iterable; consuming them as-if they were streams. So we could use an array instead for the same effect:
.mergeMap((data) => [loaded(data), otherAction(data)])
Which one you use depends on your personal style preferences and use case.
Or maybe you have three actions,
API_REQUEST,
API_RUNNING,
API_SUCCESS,
searchEpic = (action$) =>
action$
.ofType('API_REQUEST')
.mergeMap((action) => {
return concat(
of({type: 'API_RUNNING'}),
Observable.fromPromise(searchPromise)
.map((data) => {
return {type: 'API_SUCCESS', data};
}),
);
});
In the example below I was wondering how you would go about preforming two operations on the same response from the .swichMap().
In the example I put the second .map in which is clearly wrong but sort of illiterates what I want to do. How would I go about calling two functions. Also when I break the fist map() out into a function like .map(response => {fn1; fn2;}); typescript throws an error?
#Effect()
getUserCourse$: Observable<Action> = this.actions$
.ofType(userCourse.ActionTypes.LOAD_USER_COURSE)
.map<string>(action => action.payload)
.switchMap(userCourseId => this.userCourseApi.getUserCourse(userCourseId))
.map(response => new userCourse.LoadUserCourseSuccessAction(response.data));
.map(response => new course.LoadCourseSuccessAction(response.course));
For this answer I'm assuming that both functions userCourse.LoadUserCourseSuccessAction and course.LoadCourseSuccessAction do return Observables. If not you can always create one with Rx.Observable.of or Rx.Observable.fromPromise in case of for example an AJAX call.
If I understand you correctly you want to do independent things with the response, but do them in parallel and merge the results back in the stream. Have a look at the following code that shows how this can be archived.
Rx.Observable.of(
{data: 'Some data', course: 'course1'},
{data: 'Some more data', course: 'course2'}
).mergeMap((obj) => {
// These two streams are examples for async streams that require
// some time to complete. They can be replaced by an async AJAX
// call to the backend.
const data$ = Rx.Observable.timer(1000).map(() => obj.data);
const course$ = Rx.Observable.timer(2000).map(() => obj.course);
// This Observable emits a value as soon as both other Observables
// have their value which is in this example after 2 seconds.
return Rx.Observable.combineLatest(data$, course$, (data, course) => {
// Combine the data and add an additinal `merged` property for
// demo purposes.
return { data, course, merged: true };
});
})
.subscribe(x => console.log(x));
Runnable demo