reduxjs / redux

A JS library for predictable global state management
https://redux.js.org
MIT License
60.91k stars 15.27k forks source link

Correct way to clear asynchronous Promise middleware #2099

Closed dagda1 closed 8 years ago

dagda1 commented 8 years ago

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() }));
  }

if 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 componentWillMount of the parent component and the error will be handled in the Promise.catch of the callApi method that happens in the middleware.

      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()
          }
        }))
      });
    };

This is because it is happening with in the same tick of the event loop. If I introduce some asynchronicity in the Autcomplete componentWIllMount function then the error is not handled in the Promise catch handler of the middleware

    class Autocomplete extends Component{
      componentWillMount() {
        setTimeout(() => {
          this.props.dispatch(actions.init({ props: this.exportProps() }));
        });
      }

This is presumably why Redux loop and Redux saga. What is the best way of ensuring that the asynchronous Promise in the middleware happens within its own eventloop tick?

markerikson commented 8 years ago

This is a usage question, and not really anything related to Redux itself. You're better off asking this on Stack Overflow.

dagda1 commented 8 years ago

thank you for your response. very helpful

On Sun, 13 Nov 2016 at 17:45, Mark Erikson notifications@github.com wrote:

Closed #2099 https://github.com/reactjs/redux/issues/2099.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/reactjs/redux/issues/2099#event-857062967, or mute the thread https://github.com/notifications/unsubscribe-auth/AAHOOPLyHFk_h1evlC1CBLNW0XqhYy4hks5q90zWgaJpZM4KwrVc .

dagda1 commented 8 years ago

I have posted this question on stack overflow but nobody answers.

I am just looking for a sentence or two of guidance, nothing in depth or code examples.

Just enough to point me in the right direction.