Is there a way to cancel an action or ignore it?
Or rather what is the best/recommended way to ignore an action?
I have the following action creator and when I input an invalid size (say 'some_string'
) into the action creator, in addition to getting my own warning message I also get:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
import { SET_SELECTED_PHOTOS_SIZE } from './_reducers';
export default (size=0) => {
if (!isNaN(parseFloat(size))) {
return {
type: SET_SELECTED_PHOTOS_SIZE,
size: size,
};
} else {
app.warn('Size is not defined or not a number');
}
};
I've discussed this in the redux
-channel in Discord (reactiflux) where one suggestion was to use redux-thunk like this:
export default size => dispatch => {
if (!isNaN(parseFloat(size))) {
dispatch({
type: SET_SELECTED_PHOTOS_SIZE,
size: size,
});
} else {
app.warn('Size is not defined or not a number');
}
}
The other option was to ignore the action inside the reducer. This does make the reducer "fatter" because it then has more responsibilities, but it uses less thunk-actions which makes it easier to debug. I could see the thunk-pattern getting out of hand since I would be forced to use it for almost every action, making batched actions a bit of a pain to maintain if you have lots of them.
Ignoring actions in Action Creators is basically a way of treating them as Command Handlers, not Event Creators. When the User clicks the button it’s some kind of Event though.
So there are basically two ways how to solve the issue:
The condition is inside action creator and thunk-middleware
is used
const cancelEdit = () => (dispatch, getState) => {
if (!getState().isSaving) {
dispatch({type: CANCEL_EDIT});
}
}
The condition is inside reducer and no middleware is required
function reducer(appState, action) {
switch(action.type) {
case: CANCEL_EDIT:
if (!appState.isSaving) {
return {...appState, editingRecord: null }
} else {
return appState;
}
default:
return appState;
}
}
I strongly prefer treating UI interaction as Events instead of Commands and there two advantages:
All your domain logic stays in the synchronous pure reducers which are very easy to test. Just imagine you would need to write unit test for the functionality.
const state = {
isSaving: true,
editingRecord: 'FOO'
};
// State is not changed because Saving is in progress
assert.deepEqual(
reducer(state, {type: 'CANCEL_EDIT'}),
state
);
// State has been changed because Saving is not in progress anymore
assert.deepEqual(
reducer({...state, isSaving: false}),
{isSaving: false, editingRecord: null}
);
As you can see the test is really simply when you treat the interaction as an Event
When you think about any interaction with the UI as an Event then you will get the best possible replay experience, because Events can’t be denied they have just happened.