Should I use one or several action types to repres

2019-03-25 09:20发布

问题:

I'm building a front-end for a search system where almost all user actions need to trigger the same async action to re-fetch search results. For example, if a user enters a keyword, then we need to fetch /api/search?q=foo, and if they later select a category we fetch /api/search?q=foo&categoryId=bar. I originally had separate action types for FETCH_RESULTS, SELECT_CATEGORY, DESELECT_CATEGORY, etc. I created one asynchronous action creator for FETCH_RESULTS, but the others are synchronous. The more I think about it, they all end up needing to re-fetching the results from the backend and update the app state based on the response from the backend.

Would it make sense for me to use the single async action-creator for any change? Or would it be better to use async action creators for each distinct user action (selecting a keyword, category, or filter)?

I think the advantage of granular actions would be the events more accurately reflect what the user did (e.g. the user selected a category) vs having to peer into the payload to figure out what actually changed, but they are all pretty similar.

回答1:

This is of course something only you can really answer based on what you know about the project. I don't think that there is any inherent advantage to having the actions be more granular, and if there aren't any, its not worth the extra effort. I would have a generic FILTER_CHANGED event and not worry about being able to see what specifically changed--presumably the action isn't going to be complicated, so I'm not going to be debugging the action a lot. As the filter state becomes more complicated and diverse, it might make more sense to break out the actions. By default though, I don't really see much value.



回答2:

I fully agree with Nathan’s answer.

I just want to add that in order to tell whether actions A and B are really one or two actions, you need to ask yourself: “If I change how some reducers react to A, will I also need to change how they react to B?”

When the handlers change together in the reducer code, it’s likely they should be a single action. When their changes may not affect each other, or if many reducers handle just one of them but not the other, they should probably stay separate.



回答3:

I agree with Dan Abramov: if the text and categories are highly coupled in your interface, just fire FETCH_RESULTS with the text and categories as action payload.

If the text input and categories selection widget do not share a close parent component, it is complicated to fire a FETCH_RESULTS which contains the text and categories (unless passing a lot of props down the tree...): you then need the action granularity.

One pattern that I have found helpful when such granularity is needed is the Saga / Process manager pattern. I've written a bit about it here: https://stackoverflow.com/a/33501899/82609

Basically, implementing this on redux would mean there's a very special kind of reducer that can trigger side-effects. This reducer is not pure, but do not have the purpose of triggering React renderings, but instead manage coordination of components.

Here's an example of how I would implement your usecase:

function triggerSearchWhenFilterChangesSaga(action,state,dispatch) {
    var newState = searchFiltersReducer(action,state);
    var filtersHaveChanged =  (newState !== state);
    if ( filtersHaveChanged )  {
        triggerSearch(newFiltersState,dispatch)
    }
    return newState;
}


function searchFiltersReducer(action,state = {text: undefined,categories: []}) {
    switch (action.type) {
        case SEARCH_TEXT_CHANGED:
            return Object.assign({}, state, {text: action.text});
            break;
        case CATEGORY_SELECTED:
            return Object.assign({}, state, {categories: state.categories.concat(action.category) });
            break;
        case CATEGORY_UNSELECTED:
            return Object.assign({}, state, {categories: _.without(state.categories,action.category) });
            break;
    }
    return state;
}

Note if you use any time-traveling (record/replay/undo/redo/whatever) debugger, the saga should always be disabled when replaying actions because you don't want new actions to be dispatched during the replay.

EDIT: in Elm language (from which Redux is inspired) we can perform such effects by "reducing" the effects, and then applying them. See that signature: (state, action) -> (state, Effect)

There is also this long discussion on the subjet.

EDIT:

I did not know before but in Redux action creators can access state. So most problems a Saga is supposed to resolve can often be solved in the action creators (but it creates more unnecessary coupling to UI state):

function selectCategory(category) {
  return (dispatch, getState) => {
    dispatch({type: "CategorySelected",payload: category});
    dispatch({type: "SearchTriggered",payload: getState().filters});
  }
}


标签: reactjs redux