dagda1 dagda1 - 23 days ago 11
React JSX Question

Promise.catch in redux middleware being invoked for unrelated reducer

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.

Answer

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:

  1. Middleware: Dispatch fetchTeam success inside Promise.then
  2. Redux update props
  3. React: render new props
  4. React: componentWillMount
  5. 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.

Comments