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 :)