Promise.catch in redux middleware being invoked for unrelated reducer - reactjs

I have the following middleware that I use to call similar async calls:
import { callApi } from '../utils/Api';
import generateUUID from '../utils/UUID';
import { assign } from 'lodash';
export const CALL_API = Symbol('Call API');
export default store => next => action => {
const callAsync = action[CALL_API];
if(typeof callAsync === 'undefined') {
return next(action);
}
const { endpoint, types, data, authentication, method, authenticated } = callAsync;
if (!types.REQUEST || !types.SUCCESS || !types.FAILURE) {
throw new Error('types must be an object with REQUEST, SUCCESS and FAILURE');
}
function actionWith(data) {
const finalAction = assign({}, action, data);
delete finalAction[CALL_API];
return finalAction;
}
next(actionWith({ type: types.REQUEST }));
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I am then making the following calls in componentWillMount of a component:
componentWillMount() {
this.props.fetchResults();
this.props.fetchTeams();
}
fetchTeams for example will dispatch an action that is handled by the middleware, that looks like this:
export function fetchTeams() {
return (dispatch, getState) => {
return dispatch({
type: 'CALL_API',
[CALL_API]: {
types: TEAMS,
endpoint: '/admin/teams',
method: 'GET',
authenticated: true
}
});
};
}
Both the success actions are dispatched and the new state is returned from the reducer. Both reducers look the same and below is the Teams reducer:
export const initialState = Map({
isFetching: false,
teams: List()
});
export default createReducer(initialState, {
[ActionTypes.TEAMS.REQUEST]: (state, action) => {
return state.merge({isFetching: true});
},
[ActionTypes.TEAMS.SUCCESS]: (state, action) => {
return state.merge({
isFetching: false,
teams: action.payload.response
});
},
[ActionTypes.TEAMS.FAILURE]: (state, action) => {
return state.merge({isFetching: false});
}
});
The component then renders another component that dispatches another action:
render() {
<div>
<Autocomplete items={teams}/>
</div>
}
Autocomplete then dispatches an action in its componentWillMount:
class Autocomplete extends Component{
componentWillMount() {
this.props.dispatch(actions.init({ props: this.exportProps() }));
}
An error happens in the autocomplete reducer that is invoked after the SUCCESS reducers have been invoked for fetchTeams and fetchResults from the original calls in componentWillUpdate of the parent component but for some reason the catch handler in the middleware from the first code snippet is invoked:
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I do not understand why the catch handler is being invoked as I would have thought the promise has resolved at this point.

Am not completely sure, it's hard to debug by reading code. The obvious answer is because it's all happening within the same stacktrace of the call to next(actionWith({ type: types.SUCCESS, payload: { response } })).
So in this case:
Middleware: Dispatch fetchTeam success inside Promise.then
Redux update props
React: render new props
React: componentWillMount
React: Dispatch new action
If an error occurs at any point, it will bubble up to the Promise.then, which then makes it execute the Promise.catch callback.
Try calling the autocomplete fetch inside a setTimeout to let current stacktrace finish and run the fetch in the next "event loop".
setTimeout(
() => this.props.dispatch(actions.init({ props: this.exportProps() }))
);
If this works, then its' the fact that the event loop hasn't finished processing when the error occurs and from the middleware success dispatch all the way to the autocomplete rendered are function calls after function calls.
NOTE: You should consider using redux-loop, or redux-saga for asynchronous tasks, if you want to keep using your custom middleware maybe you can get some inspiration from the libraries on how to make your api request async from the initial dispatch.

Related

Using Mocha/Chai to test React-Redux app with an Axios async call

I have a connected component which does this:
componentWillMount() {
this.props.fetchMessage();
this.props.fetchPlots();
}
The first function returns a string, the second an array of objects.
First function:
export function fetchMessage() {
return function (dispatch) {
axios.get(ROOT_URL, {
headers: {
authorization: localStorage.getItem('token')}
})
.then(response => {
dispatch({
type: FETCH_MESSAGE,
payload: response.data.message
});
});
}
}
The second function is very similar.
Types are like this:
export const FETCH_MESSAGE = 'fetch_message';
Reducers follow this format:
case FETCH_MESSAGE:
return { ...state, message: action.payload }
case FETCH_PLOTS:
return { ...state, plots: action.payload }
I want to test it with Mocha and Chai. I'm trying to pass state like this:
beforeEach(() =>
{
component = renderComponent(Component, null, {
message: 'teststring',
plots: [
{...},{...},{...}
]
});
});
at which point, I get the following error:
Error: Actions must be plain objects. Use custom middleware for async actions.
at dispatch (C:...\node_modules\redux\lib\createStore.js:165:13)
at Object.fetchMessage (C:...\node_modules\redux\lib\bindActionCreators.js:7:12)
at Dashboard.componentWillMount (C:.../src/components/component.js:9:20)
How can I simulate the action/pass the object into state without it puking on me?

Manipulating Redux output

I'm working on a React application to render content of a WordPress website using the WordPress Rest API, Redux and Thunk.
The WordPress API returns posts without detailed information about the category (name, slug, etc). All I'm getting is the id. I'm currently calling an additional action/function to get the detailed category information (output). Below an example of how I'm currently fetching my posts.
// Actions.js
import axios from 'axios'
export const fetchPosts = (page = 1) => {
return {
type: "FETCH_POSTS",
payload: axios.get(`${REST_URL}/wp/v2/posts?per_page=14&page=${page}`)
}
}
|
// PostsReducer.js
const initialState = {
posts: [],
fetching: false,
fetched: false,
error: null
}
export default function reducer(state=initialState, action) {
switch (action.type) {
case "FETCH_POSTS": {
return {
...state,
fetching: true
}
}
case "FETCH_POSTS_REJECTED": {
return {
...state,
fetching: false,
error: action.payload
}
}
case "FETCH_POSTS_FULFILLED": {
return {
...state,
fetching: false,
fetched: true,
posts: action.payload
}
}
}
return state
}
And this is how I'm fetching category information:
export const fetchCategory = (id) => {
return {
type: "FETCH_CATEGORY",
payload: axios.get(`${REST_URL}/wp/v2/categories/${id}`)
}
}
Is there a way to combine my fetchPosts() action with the fetchCategory() action, so it populates the post.categories, returned from fetchPosts() with the more detailed fetchCategory() information?
If you're referring for ajax calls chaining you can use this example to understand how thunk can work for you:
function loadSomeThings() {
return dispatch => {
fetchFirstThingAsync.then(data => { // first API call
dispatch({ type: 'FIRST_THING_SUCESS', data }); // you can dispatch this action if you want to let reducers take care of the first API call
return fetchSecondThingAsync(data), // another API call with the data received from the first call that returns a promise
})
.then(data => {
dispatch({ type: 'SECOND_THING_SUCESS', data }); // the reducers will handle this one as its the object they are waiting for
});
};
}
Basically when we call loadSomeThings we dispatch an new action as a function (fetchFirstThingAsync) as our first ajax call, redux-thunk will catch that before any reducer does as function are not the plain object that reducers can handle, thunk will invoke this function with dispatcher as an argument (along getState and some more args), we wait it out with .then and then we can dispatch a plain object that reducers can handle + returning another promise (fetchSecondThingAsync) that's your second ajax call, we wait it out with .then and again dispatching a plain object that reducers can handle.

Thunks in React-Redux - Not Resolving?

I am using React and Redux to create a login system with Google Firebase. I am trying to understand how to use thunks. I am calling my action createUser from my React component however, I'm not able to handle the callback successfully.
Here is the component function I am calling the action from:
registerUser() {
let email = this.state.user.email;
let pw= this.state.user.password;
this.props.actions.createUser(email, pw)
.then((user) => {
debugger; // The async request is successful but execution doesn't pause here
})
.catch((error) => {
debugger; // Instead I receive an error here that says, "Uncaught (in promise) RangeError: Maximum call stack size exceeded"
});
}
Here are the actions:
export function createUserSuccess(user) {
debugger;
return { type: types.CREATE_USER_SUCCESS, payload: { registeredUser: user, registerMsg: 'Successful Registration!' }};
}
export function createUserError(error) {
return { type: types.CREATE_USER_ERROR, payload: { registeredUser: {}, registerMsg: error.message }};
}
export function createUser(email, pw) { // async thunk
debugger;
return (dispatch) => {
debugger;
return firebase.auth().createUserWithEmailAndPassword(email, pw)
.then((user) => {dispatch(createUserSuccess(user))}) // todo: figure out why this won't resolve
.catch(error => dispatch(createUserError(error)));
}
}
And my Reducer:
import * as types from '../actions/actionTypes';
import initialState from './initialState';
export default function registerReducer(state = initialState.registeredUser, action) {
debugger;
switch (action.type) {
case types.CREATE_USER_SUCCESS:
return [
...state, // es6 spread operator - explodes all values in array
Object.assign({}, action.payload)
];
case types.CREATE_USER_ERROR:
return [
...state,
Object.assign({}, action.payload)
];
default:
return state;
}
}
I know the actual request to Google firebase is OK because the createUserSuccess action creator gets fired. Why isn't execution stopping at the appropriate place in my React Component?
You can check here this implementation
The Service when we read the user auth and set the value to Redux
https://github.com/x-team/unleash/blob/develop/app/services/authService.js
The reducer when set the user state to the redux state object
https://github.com/x-team/unleash/blob/develop/app/reducers/userReducer.js
The action creators
https://github.com/x-team/unleash/blob/develop/app/actions/UserActions.js
The most important part is the authService, let me know any question

react - "Reducers may not dispatch actions." after navigating to a route

I'm using
hashHistory.push('/home')
To navigate to some route on my app. However, whenever I use it I get
Reducers may not dispatch actions.
How can I properly navigate to some route without getting this error?
Assuming you are using Redux, you can use the thunk middleware and react-router-redux
https://github.com/gaearon/redux-thunk
https://github.com/reactjs/react-router-redux
This will allow you to dispatch an action, reduce it and then dispatch another action afterwards
import { push } from 'react-router-redux'
const someAction = (somePayload) => (dispatch, getState) => {
dispatch({
type: 'SOME_ACTION',
payload: {
somePayload
}
});
// Get the updated state from the store and navigate
let { someVar } = getState();
if (someVar === 'some value') {
dispatch(push('/home'));
}
};
// Call like so
store.dispatch(someAction())
If you're not using redux or don't want to go that route, make sure you are not dispatching an action inside a reducer or part of that action cycle
not the best solution but it works. inside reducer
setTimeout(()=>{
store.dispatch(
//
)
},1);
In my case, I was dispatching an action right after my reducer, through my http request, which was set to async: false:
const reducer = (state, action) => {
...
makeRequest();
return {
...state
}
}
makeRequest = () => {
$.ajax({
type: 'POST',
url: `MYURL`,
async: false,
data: {
...
},
success: function(response) {
store.dispatch({
// new action dispatch here...
});
}
}
)}
By changing async to true async: true, I was able to dispatch an action on the successful case of my ajax request.
I have faced this problem after upgrading react-redux 7.1.0, redux-form to 8.3.8 and the solution is to pass mapDispatchToProps as a function not an object like the snippet below.
On some pages, it is okay with an object but if you face a problem convert it to function.
More info, read https://react-redux.js.org/using-react-redux/connect-mapdispatch.
const mapDispatchToProps = (dispatch) => {
return {
// dispatching plain actions
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
reset: () => dispatch({ type: 'RESET' }),
}
}
If it solved your issue, please mark my answer as a correct answer.

How to call promise function in reducer?

I am call a promise function in my reducer, so I call it like this:
cart(state, action) {
switch(action.type) {
//...
case LOG_IN:
return getCartProducts(true)
}
}
getCartProducts(isLogin) {
// ....
return cartApi.list({
customerId: "",
items: items
}).then(data => {
return cartReduce(undefined, receiveCartProducts(data.items, false))
})
}
so this just returns a promise, not the object I want to
You should make async actions for these as described in the Redux docs.
import fetch from 'isomorphic-fetch';
// One action to tell your app you're fetching data
export const GET_CART_PRODUCTS_REQUEST = 'GET_CART_PRODUCTS_REQUEST'
function getCartProductsRequest(customerId) {
return {
type: GET_CART_PRODUCTS_REQUEST,
customerId
};
}
// One action to signify success
export const GET_CART_PRODUCTS_SUCCESS = 'GET_CART_PRODUCTS_SUCCESS'
function getCartProductsSuccess(cartProducts) {
return {
type: GET_CART_PRODUCTS_SUCCESS,
cartProducts
};
};
// One action to signify an error
export const GET_CART_PRODUCTS_ERROR = 'GET_CART_PRODUCTS_ERROR'
function getCartProductsSuccess(error) {
return {
type: GET_CART_PRODUCTS_ERROR,
error
};
};
// This is the async action that fires one action when it starts
// And then one of two actions when it finishes
export function fetchCartProducts(customerId) {
return dispatch => {
// Dispatch action that tells your app you're going to get data
// This is where you'll set any 'isLoading' state in your store
dispatch(getCartProductsRequest(customerId))
return fetch('https://api.website.com/customers/' + customerId + '/cartProducts')
.then(response => response.json())
.then(
// Fire success action on success
cartProducts => dispatch(getCartProductsSuccess(cartProducts)),
// Fire error action on error
error => dispatch(getCartProductsError(error))
);
};
};
}

Resources