How to cancel/ignore an action in redux
Asked Answered
C

1

18

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.

Casto answered 9/12, 2015 at 17:3 Comment(0)
T
14

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:

  1. The condition is inside action creator and thunk-middleware is used

    const cancelEdit = () => (dispatch, getState) => {
      if (!getState().isSaving) {
        dispatch({type: CANCEL_EDIT});
      }
    }
    
  2. 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:

  1. 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

  1. What if you decided that instead of ignoring the action you would rather show some visual indication that the action is not possible? You would need to dispatch another action or basically rebuild it. However, you can’t use hot-reload with replay here because the logic in action creator is not re-playable. If the logic is in reducer though, you can simply change the behaviour, the reducer will get hot-reloaded and all the events gets replayed. The only event that you dispatch is that user clicked some button and you can’t deny that fact. So unless you drastically change the UI you can always hot-reload with replay.

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.

Turning answered 9/12, 2015 at 17:13 Comment(3)
Regarding the hot reloading aspect, if you replay the actions as they are received by the reducer then you should end up with the same state either way, and if other actions get triggered in the creator then replaying those should still result in the same state, assuming the reducer is pureSarajane
This is true, but as I explained in the example above. In the first approach (using thunk-middleware) the action is not even dispatched and therefore the information is lost and the information is that User clicked the button. This is a trivial example but in real world you might end up with really heavy logic in action creators which results in many Actions (Events) but the logic for generating the actions is not replayable anyway. However, the fact that some UI interaction has been made can't be denied (clicking the button).Ashaashamed
To ignore actions in redux, you should take a look at redux-ignore. It was recommended by Dam Abramov himself.Astonish

© 2022 - 2024 — McMap. All rights reserved.