redux-observable dispatch actions - redux

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

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
}

Redux - Update store with same function from different files

being rather new to react.js + redux, I'm facing the following conundrum:
I have multiple files, which need to update the store in exactly the same way, based on the stores current state. Currently I simply copy-paste the same code (along with the needed mapStateToProps), which goes again DRY.
Similar to something like the below, where getData is an Ajax call living in the actions file and props.timeAttribute is coming from mapStateToProps:
props.getData(props.timeAttribute).then((newState) => {
console.log(newState)
})
Would a function like that go in the actions file? Can the current state be read from within that actions file? Or does one normally create some sort of helperFile.js in which a function like that lives and is being called from other files?
Thanks!
If your file is executing the same action, then yes, you would put the action creator in a separate file and export it. In theory, you can put state in an action by passing the state as a parameter, but the philosophy behind an action is that it announces to your application that SOMETHING HAPPENED (as denoted by the type property on the return value of the action function). The reducer function responsible for handling that type subsequently updates the state.
You can access the current state of the store inside of an action creator like this:
export const testAction = (someParam) => {
return (dispatch, getState) => {
const {
someState,
} = getState(); //getState gets the entire state of your application
//do something with someState and then run the dispatch function like this:
dispatch(() => {type: ACTION_TYPE, payload: updatedState})
}
I like this approach because it encapsulates all the logic for accessing state inside of the one function that will need to access it.
DO NOT modify the state inside of the action creator though! This should be read only. The state of your application should only be updated through your reducer functions.
Yes, it is recommended to maintain a separate file for your actions.
Below is an example of how i use an action to fetch information and dispatch an action.
export const fetchComments = () => (dispatch) => {
console.log("Fetch Comment invoked");
/*you can use your Ajax getData call instead of fetch.
Can also add parameters if you need */
return fetch(baseUrl + 'comments')
.then(response => {
if (response.ok){
return response;
}
else {
var error = new Error('Error ' + response.status + ': ' + response.statusText);
error.response = response;
throw error;
}
},
error => {
var errmess = new Error(error.message);
throw errmess;
})
.then(response => response.json())
.then(comments => dispatch(addComments(comments)))
.catch(error => dispatch(commentsFailed(error.message)));
}
/* Maintain a separate file called ActionTypes.js where you can store all the ActionTypes as Strings. */
export const addComments = (comments) => ({
type : ActionTypes.ADD_COMMENTS,
payload : comments
});
export const comments = (errMess) => ({
type : ActionTypes.COMMENTS_FAILED,
payload : errMess
});
Once, you receive dispatch an action, you need an reducer to capture the action and make changes to your store.
Note that this reducer must be a pure function.
export const comments = (state = { errMess: null, comments:[]}, action) => {
console.log("inside comments");
switch (action.type) {
case ActionTypes.ADD_COMMENTS:
return {...state, errMess: null, comments: action.payload};
case ActionTypes.COMMENTS_FAILED:
return {...state, errMess: action.payload};
default:
return state;
}
};
Don't forget to combine the reducers in the configureStore().
const store = createStore(
combineReducers({
comments
}),
applyMiddleware(thunk,logger)
);
In your components where you use the Actions, use
const mapDispatchToProps = dispatch => ({
fetchComments : () => dispatch(fetchComments()),
})
Note to export the component as
export default connect(mapStateToProps,mapDispatchToProps)(Component);

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

Perform Ajax Fetch in a Redux Reducer?

I'm trying to wrap my head around accessing the state inside Redux actionCreators; instead did the following (performed ajax operation in the reducer). Why do I need to access the state for this — because I want to perform ajax with a CSRF token stored in the state.
Could someone please tell me if the following is considered bad practice/anti-pattern?
export const reducer = (state = {} , action = {}) => {
case DELETE_COMMENT: {
// back-end ops
const formData = new FormData();
formData.append('csrf' , state.csrfToken);
fetch('/delete-comment/' + action.commentId , {
credentials:'include' ,
headers:new Headers({
'X-Requested-With':'XMLHttpRequest'
}) ,
method:'POST' ,
body:formData
})
// return new state
return {
...state ,
comments:state.comments.filter(comment => comment.id !== action.commentId)
};
}
default: {
return state;
}
}
From the redux documentation:
The only way to change the state is to emit an action, an object describing what happened. Do not put API calls into reducers. Reducers are just pure functions that take the previous state and an action, and return the next state. Remember to return new state objects, instead of mutating the previous state.
Actions should describe the change. Therefore, the action should contain the data for the new version of the state, or at least specify the transformation that needs to be made. As such, API calls should go into async actions that dispatch action(s) to update the state. Reducers must always be pure, and have no side effects.
Check out async actions for more information.
An example of an async action from the redux examples:
function fetchPosts(subreddit) {
return (dispatch, getState) => {
// contains the current state object
const state = getState();
// get token
const token = state.some.token;
dispatch(requestPosts(subreddit));
// Perform the API request
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
// Then dispatch the resulting json/data to the reducer
.then(json => dispatch(receivePosts(subreddit, json)))
}
}
As per guidelines of redux.
It's very important that the reducer stays pure. Things you should never do inside a reducer:
Mutate its arguments;
Perform side effects like API calls and routing transitions;
Call non-pure functions, e.g. Date.now() or Math.random().
If you are asking whether it is anti-pattern or not then yes it is absolutely.
But if you ask what is the solution.
Here you need to dispatch async-action from your action-creators
Use "redux-thunk" or "redux-saga" for that
You can access the state and create some async action
e.g inside your action-creator ( Just for example )
export function deleteCommment(commentId) {
return dispatch => {
return Api.deleteComment(commentId)
.then( res => {
dispatch(updateCommentList(res));
});
};
}
export function updateCommentList(commentList) {
return {
type : UPDATE_COMMENT_LIST,
commentList
};
}
Edit: You can access the state -
export function deleteCommment(commentId) {
return (dispatch, getState) => {
const state = getState();
// use some data from state
return Api.deleteComment(commentId)
.then( res => {
dispatch(updateCommentList(res));
});
};
}

Redux timeout thunk error "Actions must be plain objects"

I'm using redux-thunk and I also want to dispatch some actions with timeout. Because of some reasons (i want all timeouts in an object, i want to able to cancel them, doesnt really matter now) I want to have custom 'timeout middleware' and 'action enchancer'
enchancer just emits special type of action:
const addTimeoutToAction = (delay, action) => ({
type: 'TIMEOUT'
, delay
, action
})
middleware just catches it and should dispatch action after timeout ends
({dispatch, getState}) => next => action => {
if (action && action.type === 'TIMEOUT') {
setTimeout(() => {
dispatch(action.action);
}, action.delay)
}
next(action);
}
So my expectation is that dispatch function in the middleware will send action back to the middleware chain, where it will start to go through all again.
My example code works with plain action, however thunked action is not. please help me understand how to reroute delayed action back to middleware chain.
Example code:
http://codepen.io/Fen1kz/pen/zKadmL?editors=0010
You code should look like this
const action3 = () => (dispatch, getState) => {
dispatch({
type: 'action3'
});
}
Whenever you use thunk middleware, you MUST call dispatch to dispatch actions, you cannot return an object.
Here is the corrected codepen: http://codepen.io/anon/pen/pEKWRK?editors=0010
Hope this helps.

Resources