Usually I use redux-saga, but currently I need redux-thunk. I'm using ducks for modular structure and now for example I have two ducks: auth and user with async actions below:
auth-duck.js
register(credentials) {
return dispatch => {
dispatch(actions.registerRequest());
return service.requestRegister(credentials)
.then((response) => {
dispatch(actions.registerSuccess(...));
// Here I need to dispatch some action from user-duck.js
})
.catch(() => dispatch(actions.registerError(...)))
}
}
user-duck.js
fetchUser() {
return dispatch => {...}
}
I really don't know how to not mess these two modules and dispatch fetchUser after successful register.
I could return register result (e.g. token or something else) to container from here it was dispatched and then using chaining dispatch fetchUser.
AuthContainer.js
_onSubmit() {
this.props.register().then(() => this.props.fetchUser);
}
But I don't know is it the best way to manage such operations with redux-thunk?
There is no rule thunks can only make one HTTP request. If you need to fetch the user after login, then fetch it.
const login = credentials => dispatch => {
fetchLogin(credentials).then(() => {
dispatch({ type: "LoginSuccess" })
return fetchUser()
}).then(() => {
dispatch({ type: "UserFetched" })
})
}
If you want to reuse the user fetch action, then dispatch a thunk from a thunk.
const fetchCurrentUser = login => dispatch => {
return fetchUser(login.userId).then(user => {
dispatch({ type: "UserLoad" })
return user
})
}
const login = credentials => dispatch => {
return fetchLogin(credentials).then(login => {
dispatch({ type: "LoginSuccess" })
return dispatch(fetchCurrentUser(login))
}
}
In one of my apps, I call 7 action thunks after successful login.
After a long search I found two options how to share the logic from separate domains.
The first one is to use mapDispatchToProps (Thanks #DonovanM), just like this:
function mapDispatchToProps(dispatch) {
return {
login: (credentials) => {
return dispatch(authActions.login(credentials)).then(
() => dispatch(userActions.fetchUser())
);
}
}
}
login function returns Promise thats why we can chain it to another one.
And the second possible option:
Use something like a "bridge" file in our case it is app-sagas.js
app-duck.js
import {actions as authActions} from './auth-duck.js';
import {actions as userActions} from './user-duck.js';
export function doLogin(credentials) {
return dispatch => {
return dispatch(authAction.login(credentials)).then(
() => dispatch(userActions.fetchUser())
);
}
}
In the second case we can place all logic into ducks and avoid spreading the logic within containers. But I guess it is possible to combine both methods.
Related
I am trying to modify an effect I have made into letting me start and stop multiple firestore queries by using two actions. Currently the effect allows me to start and stop a single firestore query by listening for two separate actions in the effect. I simply use a switchMap to switch into an empty observable when there is a stop action. This works just fine.
#Effect()
startStopQuery$ = this.actions$.pipe(
ofType(
ActionTypes.START,
ActionTypes.STOP
),
switchMap(action => {
if (action.type === ActionTypes.STOP) {
return of([]);
} else {
return this.afs.collection('collection', ref => {
return ref.where('field', '==', 'x');
}).stateChanges();
}
}),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
What I actually want to do is to have multiple queries ongoing that I can start and stop with those same two actions, but where it depends on the action payload. When I modified it everytime I performed a new query the last one stops working. I think it is because the switchMap operator switches away from my last query observable. This is the best I have come up with:
#Effect()
startStopQueryById$ = this.actions$.pipe(
ofType(
ActionTypes.START_BY_ID,
ActionTypes.STOP_BY_ID
),
switchMap(action => {
if (action.type === ActionTypes.STOP_BY_ID) {
return of([]);
} else {
return this.afs.collection('collection', ref => {
return ref.where('field', '==', action.id);
}).stateChanges();
}
}),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
As I said, I think the issue is the switchMap operator. But that is also what I depended on to make the "stop" work in the first place. I cant seem to come up with another solution as I am not very well versed in the style yet.
Any help would be greatly appreciated!
I came up with a solution. I make an object that maps ID's to the firestore statechanges observables. On the start action I make the listener and adds it to the object. I make sure that it automatically unsubscribe by piping takeUntil with the corresponding stop action. It returns a merge of all the observables in the object and I silply do as before. I also have a seperate effect triggered by the stop action to remove the observable from the object. It looks like so:
queriesById: {[id: string]: Observable<DocumentChangeAction<Element>[]>} = {};
#Effect()
startQuery$ = this.actions$.pipe(
ofType(ActionTypes.START_BY_ID),
switchMap(action => {
this.queriesByPlay[action.pid] = this.afs.collection<Element>('requests', ref => {
return ref.where('field', '==', action.id);
}).stateChanges().pipe(
takeUntil(
this.actions$.pipe(
ofType(ActionTypes.STOP_BY_ID),
filter(cancelAction => action.id === cancelAction.id),
)
)
);
return merge(
Object.values(this.queriesByPlay)
);
}),
mergeMap(actions => actions),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
Effect({dispatch: false})
stopQuery$ = this.actions$.pipe(
ofType(ActionTypes.STOP_BY_ID),
map(action => delete this.queriesByPlay[action.id]),
);
This seems to work and have no problems except for being convoluted hard to understand.
I seek help from you for the first time as I am in deep trouble.
I am using ngrx effects to load some shop items into the cart on app init from firebase realtime database. As the payload I get the whole firebase database snapshot object instead of just the shop items themselves, therefore the ngrx store receives an object it cannot understand and the app state does not change.
Check the photos please: when console.logged(), I see the exact objects that I need. But the Redux Devtools reveal the real deal, and I do not know how to fix this. Could somebody give me advice? Thank you very, very much.
The effect looks like this:
#Effect()
getShopItems: Observable<any> = this.actions.pipe(
ofType(shopActions.GET_ITEMS),
switchMap(() => {
return of(this.database.ref('cart').once('value', snap => {
snap.forEach(childSnap => childSnap.val());
// snap.forEach(childSnap =>
// console.log(childSnap.val()));
}))
.pipe(
map((payload) => {
return {
type: 'GET_ITEMS_SUCCESS',
payload: payload
};
}
));
})
);
The reducer functions for the actions in question look like:
case shopActions.GET_ITEMS: {
return {
...state,
shopItems: [...state.shopItems],
itemCount: state.shopItems.length
};
}
case shopActions.GET_ITEMS_SUCCESS: {
return {
...state,
shopItems: [action.payload],
itemCount: state.shopItems.length + 1
};
}
https://imgur.com/a/pDGAwFI
Use from rather than of:
import { from } from 'rxjs';
#Effect()
getShopItems: Observable<any> = this.actions.pipe(
ofType(shopActions.GET_ITEMS),
switchMap(() => {
return from(this.database.ref('cart').once('value', snap => {
snap.forEach(childSnap => childSnap.val());
}))
.pipe(
map((payload) => {
return {
type: 'GET_ITEMS_SUCCESS',
payload: payload
};
}
));
})
);
RxJs from - https://www.learnrxjs.io/operators/creation/from.html
I am working on a web app built on react + redux + thunk.
I have action creators as below. There are 2 async API calls, first fetchUser() will get user related info from the server, then fetchUserComments is another async call which returns me all the user comments.
export function fetchUserInfoFlow () {
return (dispatch, getState) => {
return dispatch(fetchUser()).then(() => {
return dispatch(fetchUserComments()
})
}
}
export function fetchUser() {
return (dispatch, getState) => {
return axios.get('urlhere')
.then(function (response) {
// basically fetch user data and update the store
dispatch({type: FETCH_USERS, user:response.data)
})
.catch(function (error) {
console.error(error)
})
}
}
export function fetchUserComment() {
return (dispatch, getState) => {
return axios.get('urlhere')
.then(function (response) {
// fetch user comments, get the user list from store, matching the user id and append the comments list to the user
let user = {...getState().user}
response.data.forEach(function (userComment) {
user.forEach(function (userComment) {
if(user.userId = userComment.userId){
user.comments = userComment.comments
}
}
}
dispatch({type: UPDATE_USER_COMMENT, user:response.data)
})
.catch(function (error) {
console.error(error)
})
}
}
I have several action creators similar to the above fetching user's related info and finally updating modifying the store's user list.
The problem is that the component does not re-render when the state updated, suspect if I have mutated the user list somewhere.
And I have read several articles that using getSate() in action creator should be only used in several cases and should not update its values as this will mutates the state.
In this cases, where should I put the logic of modifying the user list and how to avoid mutating the user list?
Thanks a lot!
This part should just go into your reducer, since thats the only place you should be making changes to the state:
let user = {...getState().user}
response.data.forEach(function (userComment) {
user.forEach(function (userComment) {
if(user.userId = userComment.userId){
user.comments = userComment.comments //mutating state
}
}
}
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));
});
};
}
I have an action, that uses a redux thunk, that looks like so:
export function fetchData(query) {
return dispatch => {
return fetch(`http://myapi?query=${query}` ,{mode: 'cors'})
.then(response => response.json())
.then(json => { dispatch(someOtherAction(json)) })
}
}
}
and then my someOtherAction actually updates state:
export function someOtherAction(data) {
return {
action: types.SOME_ACTION,
data
}
}
But i want it to be possible for the fetchData action creator to be reusable so that different parts of my app can fetch data from myapi and then have different parts of the state based on that.
I'm wondering what is the best way to reuse this action? Is it acceptable to pass a second parameter in to my fetchData action creator that stipulates which action is called on a successful fetch:
export function fetchData(query, nextAction) {
return dispatch => {
return fetch(`http://myapi?query=${query}` ,{mode: 'cors'})
.then(response => response.json())
.then(json => { dispatch(nextAction(json)) })
}
}
}
Or is there an accepted way of doing this sort of thing?
I use a middleware for that. I have defined the fetch call in there, then in my actions I send the URL to fetch and the actions to dispatch when completed. This would be a typical fetch action:
const POSTS_LOAD = 'myapp/POST_L';
const POST_SUCCESS = 'myapp/POST_S';
const POST_FAIL = 'myapp/POST_F';
export function fetchLatestPosts(page) {
return {
actions: [POSTS_LOAD, POST_SUCCESS, POST_FAIL],
promise: {
url: '/some/path/to/posts',
params: { ... },
headers: { ... },
},
};
}
When calling that action, the POST_LOAD action will be dispatch automatically by the middleware just before the fetch request it's executed. If everything goes well the POST_SUCCESS action will be dispatched with the json response, if something goes wrong the POST_FAIL action will be dispatched by the middleware.
All the magic it's in the middleware! And it's something similar to this:
export default function fetchMiddleware() {
return ({ dispatch, getState }) => {
return next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
const { promise, actions, ...rest } = action;
if (!promise) {
return next(action);
}
const [REQUEST, SUCCESS, FAILURE] = actions;
next({ ...rest, type: REQUEST }); // <-- dispatch the LOAD action
const actionPromise = fetch(promise.url, promise); // <-- Make sure to add the domain
actionPromise
.then(response => response.json())
.then(json => next({ ...rest, json, type: SUCCESS })) // <-- Dispatch the success action
.catch(error => next({ ...rest, error, type: FAILURE })); // <-- Dispatch the failure action
return actionPromise;
};
};
}
This way I have all my requests on a single place and I can define the actions to run after the request it's completed.
------------EDIT----------------
In order to get the data on the reducer, you need to use the action name you defined on the original action creator. The following example shows how to handle the POST_SUCCESS action from the middleware to get the posts data from the json response.
export function reducer(state = {}, action) {
switch(action.type) {
case POST_SUCCESS: // <-- Action name
return {
...state,
posts: action.json.posts, // <-- Getting the data from the action
}
default:
return state;
}
}
I hope this helps!