Reducer behavior

2019-07-16 12:01发布

问题:

As I understand it, when an action is called, all reducers respond. If action exists in the switch case statement of the reducer, it executes. If it doesn't, then the case: default executes which preserves the existing state.

When the action exists in the reducer but the particular property it's trying to update does not exist, it seems to behave OK as there's nothing to update.

For example, I have an action creator that is used to set the visible property of my modals. Each modal has its own Id. My code looks like this:

export default (state = initialState, action) => {
   case types.SET_MODAL_IS_VISIBLE:
      return Object.assign({}, state,
         { modal22: action.value }
      )}

I have the SET_MODAL_IS_VISIBLE in multiple reducers but if modal22 is not defined in a particular reducer, nothing happens and no errors.

Now, I have a scenario that is throwing an error. I have a general purpose date picker component that I built that can be used as a single and independent date picker OR it can be "linked to" another one. The second scenario is useful if I need the user to give me two dates e.g. start and end dates.

I also built a feature where if the date picker is coupled with another one, when the user sets the date in the first date picker, I disable all the dates prior to that date in the second date picker because I don't want the user to unintentionally select an end date that is prior to the start date.

I define my date pickers as below:

const initialState = {
    datePickers: {
        "startDatePicker": {
            activeDate: "8/25/2017",
            disabledBefore: "",
            linkedTo: "endDatePicker"
         },
         "endDatePicker": {
            activeDate: "",
            disabledBefore: "8/25/2017" // This date is set when the user sets the active date in startDatePicker
            linkedTo: ""
       }
    }
}

This scenario is a bit interesting because a state change in one property in my reducer is triggering a state change in another. This is not difficult to do and I have a way of controlling when I do the update.

The action for setting disabled dates looks like below:

...
case types.SET_DISABLED_DATES:
    return Object.assign({}, state,
       datePickers: Object.assign({}, state.datePickers, {
          datePickers[action.datePickerId]: Object.assign({}, state.datePickers[action.datePickerId], {
             disabledBefore: action.value
          })
    })

Please keep in mind that I can and should be able to set disabledBefore even if the date picker is used as an independent one. So, I need my SET_DISABLED_DATES in every reducer.

The problem I'm running into is that whenever I call SET_DISABLED_DATES, I get errors in reducers where the date picker is used as a single/independent one because the date picker Id for its pair is NOT defined in the reducer.

For example, in projectsReducer I may use the date picker as part of a pair so both startDatePicker and endDatePicker are defined and everything works fine.

But I may be using a single instance date picker in the tasksReducer which also responds to the SET_DISABLED_DATES call but it fails because it cannot find the endDatePicker. In this scenario, the tasksReducer is responding to the call I made to set the disabledDates property of endDatePicker in projectsReducer.

I've posted two questions about this already and the only real solution I'm seeing here is that I need to have a condition in my reducer that looks like this:

...
case types.SET_DISABLED_DATES:
   if(typeof state.datePickers[action.datePickerId] !== "undefined") { // Making sure that what I'm trying to update exists in the reducer
      return Object.assign({}, state,
       datePickers: Object.assign({}, state.datePickers, {
          datePickers[action.datePickerId]: Object.assign({}, state.datePickers[action.datePickerId], {
             disabledBefore: action.value
          })
    })
   } else {
      return state;
   }

Admittedly, this looks a bit like a kludge but I couldn't really come up with another solution here.

Again, the problem is that for as long as all reducers respond to SET_DISABLED_DATES, it's guaranteed that a particular date picker will not be there and the Object.assign() will throw an error.

Any suggestions? Is the simple condition in the reducer the way to go here? Is it a kludge?

P.S. I tried this code and it works fine and fixes the problem. On the one hand, I feel this is a bit of an anti-pattern but on the other hand, it just seems like a good idea to make sure the property I want to update in my reducer exists before attempting to update it. I'd appreciate your feedback on this. Thanks.

回答1:

You are just doing basic validation in the reducer before setting the state. That is perfectly fine. I don't think it will be a good practice to check the store in the action creator to prevent dispatching actions on objects not in the store (how would you do that anyway!).

What I don't understand is, how can a datepicker be linked to another datepicker that isn't in the store? Maybe dispatch a create and teardown action on the component's didMount and willUnmount?

I don't know your full requirements but I think we can make it a lot simpler. I'd do something like this:

The store:

{
  datePickers: {
    id1: {
      value: '',
      minValue: '',
      maxValue: '',
    },
    id2: {
      value: '',
      minValue: '',
      maxValue: '',
    }
  }
}

Now, unless you are making some kind of coupled datepicker components that will always behave in pairs, I believe the cleanest approach would be to set the disabled value in the linked datepicker in the mapDispactchToProps function in your parent component.

That is where you would set ids to the components, and you know exactly which component should be disabled before another.

Something like:

dispatch => ({
  setArrivalDate(value) {
    dispatch(datePickerActions.setValue(arrivalDateId, value);
    dispatch(datePickerActions.setMaxValue(depatureDateId, value);
  },
  setDepatureDate(value) {
    dispatch(datePickerActions.setValue(depatureDateId, value);
    dispatch(datePickerActions.setMinValue(arrivalDateId, value);
  }
})

This may not be abstract enough, but is clean.

You could do the same thing if you have a paired component, but you'd still need to know which date comes before another. It'd be a hassle to make a generic abstraction around it.



回答2:

Remove the bold part in your code below

    ...
    case types.SET_DISABLED_DATES:
       if(typeof state.datePickers[action.datePickerId] !== "undefined") { // Making sure that what I'm trying to update exists in the reducer
          return Object.assign({}, state,
           datePickers: Object.assign({}, state.datePickers, {
              datePickers[action.datePickerId]: Object.assign({}, state.datePickers[action.datePickerId], {
                 disabledBefore: action.value
              })
        })
       } else {
          return state;
       }

Also, a little bit of es6 spread and a helper switchcase function makes this code much more readable.

const newReducer = (state = defaultState, action) => switchcase({
  [types.SET_DISABLED_DATES]:
    state.datePickers[action.datePickerId] === undefined
      ? state
      : ({ ...state,
        datePickers: { ...state.datePickers,
          [action.datePickerId]: { ...state.datePickers[action.datePickerId],
            disabledBefore: action.value,
          },
        },
      }),
})(state)(action.type);

Using lodash/fp/set, the code becomes

const reducerWithLodash = (state = defaultState, action) => 
switchcase({
  [types.SET_DISABLED_DATES]:
    state.datePickers[action.datePickerId] === undefined
      ? state
      : set({...state}, `datePickers.${action.datePickerId}.disabledBefore`, action.value)
})(state)(action.type)

I haven't tested the lodash version, so please take that with a grain of salt (Dan Abramov seems to approve)