Hold epic in Redux Observable until action is received - redux

I'm new to rxjs and redux-observables
I have two epics:
export const appInit = action$ =>
action$.ofType(actions.appInitRequest)
.take(1)
.switchMap(() => actions.fetchData())
and
export const navigation = ($action, {getState}) =>
$action.ofType(actions.locationChange)
.do(console.log)
App Init fires action to fetch data,
Is it possible to hold the navigation epic till the fetch data complete? so if navigation action received and fetch data didn't complete we'll wait till fetch data complete (dispatch action.fetchDataSuccess) and than continue the navigation epic flow?
I tried the following code but than every request after the fetchDataSuccess waits for new fetchDataSuccess
export const navigation = ($action, {getState}) =>
$action.ofType(actions.locationChange)
.switchMap(({payload: {pathname}}) =>
$action.ofType(action.fetchDataSuccess)
.take(1)

Try to use withLatestFrom:
export const navigation = ($action, {getState}) =>
$action.ofType(actions.locationChange)
.withLatestFrom(appInit)
.filter(([locationChangeEvent, appInitEvent]) => {
return /* check that app inited successfully */
})
.map(([locationChangeEvent]) => locationChangeEvent)
.do(console.log)

Related

A clean way for an action to fire multiple asynchronous actions with createAsyncThunk

We're delaying the rendering of our React-Redux web app until several asynchronous app initialization tasks in the Redux store have been completed.
Here's the code that sets up the store and then fires off the initialization action:
export const setupStoreAsync = () => {
return new Promise((resolve, reject) => {
const store = setupStore()
store
.dispatch(fetchAppInitialization())
.then(unwrapResult)
.then(_ => resolve(store))
.catch(e => reject(e.message))
})
}
The promise rejection is very important since it's used to render an error message for the user in case the app cannot be properly set up. This code is very nice to read and works wonderfully.
The issue is with the action creator:
export const fetchAppInitialization = createAsyncThunk(
'app/initialization',
(_, thunkApi) =>
new Promise((resolve, reject) =>
Promise.all([thunkApi.dispatch(fetchVersionInfo())]).then(results => {
results.map(result => result.action.error && reject(result.error))
})
)
)
This code works beautifully. If any of these actions fail, the promise is rejected and the user sees an error message. But it's ugly - It's not as pretty as our normal action creators:
export const fetchVersionInfo = createAction('system/versionInfo', _ => ({
payload: {
request: { url: `/system/versionInfo` },
},
}))
We will at some point fire more than one fetch request in fetchAppInitialization, so the Promise.all function is definitely required. We'd love to be able to use Redux-Toolkit's createAction syntax to fire multiple promisified actions in order to shorten this action creator, but I have no idea if that's even possible.
Note: I'm using redux-requests to handle my axios requests.
Is createAsyncThunk even required?
Since I wasn't using the fetchAppInitialization action for anything but this single use case, I've simply removed it and moved the logic straight into the setupStoreAsync function. This is a bit more compact. It's not optimal, since the results.map logic is still included, but at least we don't use createAsyncThunk any more.
export const setupStoreAsync = () => {
return new Promise((resolve, reject) => {
const store = setupStore()
new Promise((resolve, reject) =>
Promise.all([store.dispatch(fetchVersionInfo())]).then(results => {
results.map(result => result.action.error && reject(result.error))
resolve()
})
)
.then(_ => resolve(store))
.catch(e => reject(e.message))
})
}
Update: I was able to make the code even prettier by using async/await.
export const setupStoreAsync = async () => {
const store = setupStore()
const results = await Promise.all([store.dispatch(fetchVersionInfo())])
results.forEach(result => {
if (result.action.error) throw result.error
})
return store
}

In Redux-Observable does the mapped to action only get called when initial action completes

In the code below the SET_COLOUR_RANGE action updates a value in Redux and this value is the payload of FETCH_COLOURS. So I would like the FETCH_COLOURS action to be called after the value in Redux has been updated.
Using the mapTo operator does the FETCH_COLOURS action only get called after SET_COLOUR_RANGE completes or is it called immediately after regardless?
import { selectColoursRange } from 'app/selectors/colours';
export const setColourRangeEpic = action$ =>
action$.pipe(ofType('SET_COLOUR_RANGE'), mapTo({ type: 'FETCH_COLOURS' }));
export const fetchColoursEpic = (action$, store) =>
action$
.ofType('FETCH_COLOURS')
.mergeMap(() =>
fromPromise(
axios.post(`/colour`, selectColoursRange(store.getState()))
)
.map(response => ({
type: 'FETCH_COLOURS_SUCCESS',
data: response.data
}))
.catch(error =>
Observable.of({
type: 'FETCH_COLOURS_ERROR',
error
})
)
);
The reducer will always get the action before the epic gets it.
So SET_COLOUR_RANGE goes to the reducer (thus updating the state), then it goes to setColourRangeEpic and gets mapped to a FETCH_COLOURS action. This FETCH_COLOURS action goes to the reducer, then goes to the fetchColoursEpic. By the time you call selectColoursRange(store.getState()), the state has been updated.

Run action in loop until or finished with success

I have an epic which is trigger when the user updates data about his video. The user has a single page with multiple videos on it and can do update title on any chosen video. At bottom of the page there is Submit and when the user clicks it I need to save changes to all video so I dispatch action saveVideo. This action is triggering epic which will send a request and on success should trigger next pending video on the list to be saved.
Epic:
const updateVideoEpic = action$ =>
action$.ofType(videoTypes.UPDATE_VIDEO_REQUEST)
.switchMap(action => Observable.from(updateVideo(action.payload)))
.takeUntil(action$.ofType(videoTypes.UPDATE_VIDEO_REQUEST_CANCEL))
.map(video => videoAction.updateVideoSuccess(video))
.catch((error) => Observable.of(videoAction.updateVideoFailure(error)));
How I can trigger asynchronous epic so after success updating data of 1st video will trigger again the same epic to update data for next video.
I thought about creating another epic but how you check if everything passed.
Sudo code:
const updateVideos = action$ =>
action$.ofTypes(videoTypes.UPDATE_VIDEO_BATCH_REQUEST)
.switchMap(action => action.payload.videos)
.map(video => videoAction.updateVideoBatchRequest(video));
I change a little bit of approach and decided to use only fetch and loop though requests:
// Import stylesheets
import './style.css';
import { fromPromise } from 'rxjs/observable/fromPromise';
import { from } from 'rxjs/observable/from';
import { mergeMap, catchError, switchMap } from 'rxjs/operators';
import { zip } from 'rxjs/observable/zip';
const urls = [
{ url: 'https://bbc.co.uk', name: 'BBC'},
{ url: 'https://google.com', name: 'Google'},
{ url: 'https://amazon.com', name: 'Amazon'}
];
const items$ = from(urls).pipe(
mergeMap(item => fetch(item.url, {mode: 'no-cors'}))
)
items$.subscribe(
url => console.log(url),
error => console.log(error),
() => console.log('Complete')
);
Working copy:
Working copy on stackblitz

Action on canceled observable

Hello and thanks in advance :)
Main Idea
I want to launch specific action$ epics from redux-observable depends on route and cancel them when route changes.
Also I want to handle some clean up when epics are canceled. I have done it. But:
Problem
I'm using state.dispatch(actions.signInError({})) to clean up (that is deprecated), and don't know how to do it another way. My code is below, problem at the very end.
Change epics when route is changed
/**
* Launch route specific actions if such exist and cancel previous one
*
* #param {Function} action$ - redux-observable action$
* #param {Object} state - redux state
*/
const addEpicsForRoute = (action$, state) => action$
.ofType(LOCATION_CHANGE) // action from route
.switchMap(
( action ) => {
// get epics for route
const epicsForRoute = routeEpics[ action.payload.pathname ];
if ( epicsForRoute ) {
return merge(
...epicsForRoute.map(observableCreator => observableCreator(action$, state))
);
} else {
return empty(); // no specific epics
}
}
);
Some specific epic for some route
/**
* Handle xhr request-response/error logic of sign in user
*
* #param {Function} action$
* #param {Object} state
*/
export const signIn = ( action$, state ) => {
return action$
.ofType(types.SIGN_IN_REQUEST)
.mergeMap(( { params, } ) => (
Observable.create(observer => {
services
.signInRequest( // it is ajax observable
mappers.mapSignInRequest(params)
)
.map(response => actions.signInJWTSuccess( // dispatch success
mappers.mapUser(response)
))
.catch(error => of(actions.signInError( // dispatch error
mappers.mapSignInError(error)
)))
.subscribe(( value ) => { // pass action to redux-store
observer.next(value);
});
return () => {
// cleanup logic. HERE IS A PROBLEM
// calling store.dispatch() directly in your Epics is deprecated and will be removed.
// what should I use instead?
state.dispatch(actions.signInError({}));
};
})
));
};
Also I am new for rxjs and if you have an advice how I can improve or make look code prettier I'm more than interested!
I suggest to review the flow of the code to leverage more the power of Observables operators.
An idea could be to move along these lines
export const signIn = ( action$, state ) => {
return action$
.ofType(types.SIGN_IN_REQUEST)
.switchMap(( { params, } ) => (services.signInRequest( // it is ajax observable
mappers.mapSignInRequest(params)
))
.map(response => actions.signInJWTSuccess( // dispatch success
mappers.mapUser(response)
))
.catch(error => of(actions.signInError( // dispatch error
mappers.mapSignInError(error)
)))
};
In this way you have created a function, signIn, that returns an Observable which emits the result of the signIn ajax call.
Then, I would create another piece of logic to subscribe to such Observable returned by signIn and decide what to do, e.g.
const subscription = signIn(action, state)
.subscribe(
value => {// do what needs to be done with the result of the signIn call},
err => {// insert here the logic to handle error conditions},
() => {// do here what needs to be done when the Observable completes
// consider that ajax calls complete after the first emit, therefore
// you can put this part of logic also within the first callback, the one passed as the first parameter to subscribe() method
}
)
Note that you are also storing the subscription in a variable, that you can use to unsubscribe when the route changes.
The cleanup logic you are putting in the function returned by the create method should be probably moved to the place where you actually unsubscribe the subscription because you move to another route.
I think a good question is whether this is correct expectation. When you use switchMap you unsubscribe from the inner Observable which means you no longer want to receive its emissions. So does it make sense that it'll emit yet another action when you unsubscribe?
Anyway you could merge another Observable to the chain that emits only the cleanup actions.
const action$ = new Subject();
const cleanup$ = new Subject();
action$
.pipe(
switchMap(() => new Observable(observer => {
// whatever goes here
observer.next('Observable created');
return () => {
cleanup$.next(/* create proper action here */ 'cleanup');
};
})),
merge(cleanup$),
)
.subscribe(console.log);
action$.next(1);
action$.next(2);
I'm not using real redux-observable actions but I hope you get the point.
Live demo (open console): https://stackblitz.com/edit/rxjs5-9iogn1?file=index.ts

redux-observable dispatch actions

I need to dispatch some actions in some order using redux-observable however, it takes just last action to dispatch. Please see example:
export const fetchClientsEpic = (action$, { dispatch }) =>
action$
.ofType(fetchClients)
.mapTo(fetchClientsPending(true))
.mergeMap(() => {
return ajax
.getJSON('some/get/clients/api')
.map((clients: IClient[]) => {
return fetchClientsSuccess(
map(clients, (client, index) => ({
key: index,
...client,
})),
);
});
});
fetchClientsSuccess is dispatched with clients but fetchClientsPending not, I totally do not get it why. I could use dispatch because I get it in params, but I feel it is not good solution(?). It should be done in the stream I guess. I am starting with RxJs and redux-observable. Is it possible to do?
Operators are chains of Observables where the input of one stream is the output of another. So when you use mapTo you're mapping one action to the other. But then your mergeMap maps that Pending action and maps it to that other inner Observable that does the ajax and such, effectively throwing the Pending action away. So think of RxJS as a series of pipes where data flows through (a stream)
While there is no silver bullet, in this particular case what you want to achieve can be done by using startWith at the end of your inner Observable
export const fetchClientsEpic = (action$, { dispatch }) =>
action$
.ofType(fetchClients)
.mergeMap(() => {
return ajax
.getJSON('some/get/clients/api')
.map((clients: IClient[]) => {
return fetchClientsSuccess(
map(clients, (client, index) => ({
key: index,
...client,
})),
);
})
.startWith(fetchClientsPending(true)); // <------- like so
});
This is in fact the same thing as using concat with of(action) first, just shorthand.
export const fetchClientsEpic = (action$, { dispatch }) =>
action$
.ofType(fetchClients)
.mergeMap(() => {
return Observable.concat(
Observable.of(fetchClientsPending(true)),
ajax
.getJSON('some/get/clients/api')
.map((clients: IClient[]) => {
return fetchClientsSuccess(
map(clients, (client, index) => ({
key: index,
...client,
})),
);
})
);
});
That said, I would recommend against synchronously dispatching another action to set the state that fetching is pending and instead rely on the original fetchClients action itself for the same effect. It should be assumed by your reducers that if such an action is seen, that some how the fetching still start regardless. This saves you the boilerplate and helps a bit on micro-perf since you don't need to run through the reducers, epics, and rerender twice.
There's no rules though, so if you feel strongly about this, go for it :)

Resources