How to dispatch actions from Redux middleware in correct order
Asked Answered
R

2

5

As must of the applications in the world, my React application needs to perform some Ajax calls to an API.

I've chosen to use Redux middlewares in order to correctly separate API fetching from my components.
The idea is to dispatch REQUEST actions from my components. Middlewares listen for them and dispatch SUCCESS or ERROR actions: these last are listened by the reducers.

A lot of people here have already asked how to dispatch actions from Redux middlewares: this is NOT the topic of my question here :)

First, let me show you a simple reducer I use to write:

function currentUserReduxer(state = {}, action = {}) {
  const { currentUser, error } = action.payload;

  switch (action.type) {
    case 'GET_CURRENT_USER_REQUEST':
      return { ...state, isFetching: true, error: null };

    case 'GET_CURRENT_USER_SUCCESS':
      return { ...state, id: currentUser.id, isFetching: false };

    case 'GET_CURRENT_USER_FAILURE':
      return { ...state, isFetching: false, error };

    default:
      return state;
  }
}

And the corresponding middleware:

() => next => async (action) => {
  next(action);

  switch (action.type) {
    case'GET_CURRENT_USER_REQUEST': {
      try {
        const currentUser = await api.getCurrentUser();
        next(actions.getCurrentUserSuccess(currentUser));
      } catch (error) {
        next(actions.getCurrentUserFailure(error));
      }
      break;
    }

    default:
      break;
  }
};

It worked well since a long time, and then I realized it's partially wrong: my middleware does not return the value of next, so it breaks the middleware chain and it's wrong!
Since next(action); was the first thing I've performed in the middleware, I couldn't return it this soon, so I've moved it at the end of the middleware. I've also decided to dispatch new actions instead of using next for them (after all, they ARE new actions, it does make sense to send them to the whole chain of middlewares). My new middleware look like this now:

store => next => async (action) => {
  switch (action.type) {
    case'GET_CURRENT_USER_REQUEST': {
      try {
        const currentUser = await api.getCurrentUser();
        store.dispatch(actions.getCurrentUserSuccess(currentUser));
      } catch (error) {
        store.dispatch(actions.getCurrentUserFailure(error));
      }
      break;
    }

    default:
      break;
  }

  return next(action);
};

It looks great, but I have now another issue: since store.dispatch is synchronous, next(action) is called after. That means that my reducers receive the REQUEST action AFTER the SUCCESS or FAILURE ones :(

I think one solution could be using good old promises instead of await :

store => next => async (action) => {
  switch (action.type) {
    case'GET_CURRENT_USER_REQUEST': {
      api.getCurrentUser()
        .then((currentUser) => {
          store.dispatch(actions.getCurrentUserSuccess(currentUser));
        })
        .catch((error) => {
          store.dispatch(actions.getCurrentUserFailure(error));
        });
      break;
    }

    default:
      break;
  }

  return next(action);
};

Another idea would be to wrap the store.dispatch with a setTimeout:

store => next => async (action) => {
  switch (action.type) {
    case'GET_CURRENT_USER_REQUEST': {
      try {
        const currentUser = await api.getCurrentUser();

        setTimeout(() => store.dispatch(actions.getCurrentUserSuccess(currentUser)));
      } catch (error) {
        setTimeout(() => store.dispatch(actions.getCurrentUserFailure(error)));
      }
      break;
    }

    default:
      break;
  }

  return next(action);
};

To be honest, I don't really like these two solutions, they feel so hacky...

So here is my question: how am I suppose to handle my problem? Is there a more clean way to do that?

Thanks in advance :)

Rodney answered 5/7, 2018 at 8:4 Comment(0)
P
4

I think your first solution is near to the clean answer. You can introduce the term sideEffects which are async functions executed whenever your action is dispatch. Your middleware function is still synchronize and the action will be dispatch right away. Here is an example:

getCurrentUserSideEffects = async (action) => {
    api.getCurrentUser()
        .then((currentUser) => {
          store.dispatch(actions.getCurrentUserSuccess(currentUser));
        })
        .catch((error) => {
          store.dispatch(actions.getCurrentUserFailure(error));
        });
}

<other side effects>

store => next => action => {
  switch (action.type) {
    case 'GET_CURRENT_USER_REQUEST': {
      getCurrentUserSideEffects(action);
      break;
    }

    default:
      break;
  }

  return next(action);
};

I think this idea is pretty similar to redux saga, but simpler and easier to understand.

Pentyl answered 22/4, 2019 at 15:3 Comment(0)
C
2

Seems like what you are trying to do is similar to Redux-Saga I suggest you take a look at their library.

Taken from their example

// worker Saga: will be fired on USER_FETCH_REQUESTED actions
function* fetchUser(action) {
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}

/*
  Starts fetchUser on each dispatched `USER_FETCH_REQUESTED` action.
  Allows concurrent fetches of user.
*/
function* mySaga() {
  yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}
Conceivable answered 5/7, 2018 at 8:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.