redux-observable - dispatch multiple redux actions in a single epic - javascript

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};
}),
);
});

Related

Update array state from out of a for each loop

I'm trying to update the state of an array from out of a forEach loop without loosing the previous state. I`m trying to archive something like the following:
const initialState = [{question: "a", answer: ""}, {question: "b", answer: ""}]
const [request, setRequests] = useState(initialState);
const run = () => {
request.forEach((request, idx) => {
fetch("/ask").then(data => data.json()).then(response => {
let currentState = request;
request[idx] = Object.assign(...request[idx], {answer: response.answer});
setRequests(currentState);
})
})
}
But in such a case only one response will be rendered. Any idea how to archive something like this?
The Problems
There are a couple of issues there:
You're breaking one of the fundamental rules of React state: Do not modify state directly. Doing const currentState = request; doesn't copy the object, it just makes two variables point to the same object.
You're using the version of state setter where you just pass in the update, which completely overwrites any previous outstanding state updates.
Solutions
There are (at least) two approaches here:
Do piecemeal updates as you are in that code, where earlier updates are stored in state (causing a re-render) while later updates are still in progress
or
Get all the updated information, and do a single state update (and render)
Both are valid depending on your use case.
Piecemeal Updates
For the piecemeal updates, since you're doing a bunch of state updates (which may not occur in order, depending on the vagaries of the timing of the fetch replies). Since you're updating state based on previous state (the other entries in the array), you need to use the callback version of the state setter. Then, you need to create a new array each time, and and new object within the array for the object at index idx, like this:
// *** Note I've renamed `request` to `requests` -- it's an array, it should
// use the plural, and you use `request` for an individual one later.
// I've also reversed `response` and `data`, since they were backward in the
// original. I've also added (minimal) handling of errors.
const [requests, setRequests] = useState(initialState);
const run = () => {
requests.forEach((request, idx) => {
// (It seems odd that nothing from `request` is used in the `fetch`)
fetch("/ask")
.then((response) => response.json())
.then((data) => {
setRequests((prevRequests) =>
prevRequests.map((request, index) => {
return idx === index ? { ...request, answer: data.answer } : request;
})
);
})
.catch((error) => {
// ...handle/report error...
});
});
};
map creates the new array, returning a new object for the one at index idx, or the previous unchanged ones for the others.
All At Once
The other approach is to do all the fetch calls, wait for them all to complete, and then do a single state update, like this:
// (Same naming and error-handling updates)
const run = () => {
Promise.all(requests.map((request) =>
fetch("/ask")
.then((response) => response.json())
.then((data) => {
return {...request, answer: data.answer};
})
))
.then(setRequests)
.catch((error) => {
// ...handle/report error...
});
};
You can build the new array at once and update it, something like this
const run = () => {
Promise.all(request.map((r) => fetch("/ask").then(data => data.json())))
.then((responses) => {
const newRequest = responses.map(res => ({answer: res.answer}))
setRequests(newRequest);
})
}
Using Promise.all allows you to retrieve all the json response at once, so you can build the new state in one shot.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
note: code not tested.
You problem stems from the fact that let currentState = request will keep a reference to the request at the time of making the closure. This means that it won't get updated by future calls.
You need to use the functional version of setState. Something like this:
const initialState = [{question: "a", answer: ""}, {question: "b", answer: ""}]
const [request, setRequests] = useState(initialState);
const run = () => {
request.forEach((request, idx) => {
fetch("/ask").then(data => data.json()).then(response => {
setRequests(currentState => {
currentState[request[idx]] = {answer: response.answer};
return {...currentState};
});
})
})
}

Managing promises in RXJS observables

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.

How can I sequence redux actions after some async action completed in redux-observable?

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!

Passing callback from action into Epic

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))
);

How to chain multiple .map() calls on previous items using rxjs

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

Categories