I am trying to understand how redux middleware works, during my experiment I have noticed that dispatching an action from redux middleware may result in an unexpected behavior.
I will try to explain the problem by simulating file upload as follow:
we have 3 actions:
const setProgress = (progress) => ({ type: SET_UPLOAD_PROGRESS, progress });
const setThumbnail = (thumbnail) => ({ type: SET_THUMBNAIL, thumbnail });
const calculateTotal = () => ({ type: CALCULATE_TOTAL });
Middleware to calculate total:
export const testMiddleware = (store) => (next) => (action) => {
if (action.type === 'CALCULATE_TOTAL') {
return next(action);
}
const result = next(action);
store.dispatch(calculateTotal());
return result;
};
Reducer:
const initialState = {
progress: 0,
total: 0,
thumbnail: ''
};
export function uploadReducer(state = initialState, action) {
switch (action.type) {
case SET_UPLOAD_PROGRESS:
state.progress = action.progress;
return { ...state };
case SET_THUMBNAIL:
state.thumbnail = action.thumbnail;
return { ...state };
case CALCULATE_TOTAL:
state.total += state.progress * 5;
return { ...state };
default:
return state;
}
}
here is the code for simulating file upload:
let cnt = 0;
// simulate upload progress
const setNewProgress = () => {
cnt += 2;
if (cnt > 5) return;
setTimeout(() => {
store.dispatch(setProgress(cnt * 2));
setNewProgress();
}, 1000);
};
setNewProgress();
// simulate thumbnail generating
setTimeout(() => {
store.dispatch(setThumbnail('blob:http://thumbnail.jpg'));
}, 2500);
Here is the sequence of events:
the first action works as intended and sets the progress value:
the problem starts from here; thumbnail suppose to be set by 'setThumbnail' but devtools shows that it has been set by 'calculateTotal', and every dispatch after that is mismatched:
What am I doing wrong here? is it by design? how can I dispatch an action in middleware without causing above problem?
This unexpected behavior may be cause by your uploadReducer
not being pure, i.e. it is directly operating on your state (e.g. state.progress = action.progress;
). Reducers should only return the new state and not modify existing state injected into your reducer by redux. hence, your reducer needs to look like this:
export function uploadReducer(state = initialState, action) {
switch (action.type) {
case SET_UPLOAD_PROGRESS:
return { ...state, progress: action.progress };
case SET_THUMBNAIL:
return { ...state, thumbnail: action.thumbnail };
case CALCULATE_TOTAL:
return { ...state, total: state.total + state.progress * 5 };
default:
return state;
}
}
how can I dispatch an action in middleware without causing above problem?
Your middleware looks fine (you are correctly preventing recursion and also returning the next()
result (which is not needed in your example but still makes sense in a real application). Your actions look good as well (a style remark: You could wrap your action's payload in a payload
property, which is a common convention).