ss1
ss1

Reputation: 1191

In what problems will I run if I design my reducers for async actions this way?

I have very little experience in creating frontend applications and would like to establish some conventions for myself when using Redux.

Currently my reducers for async actions look like this:

const initialState = {
    isRunning: false,
    isFinished: false,
    hasError: false,
    response: null
};
export const updatePostReducer = (state = initialState, action=null) => {
    switch (action.type) {
        case UPDATE_POST:
            return {
                isRunning: true,
                isFinished: false,
                hasError: false,
                response: null
            };
        break;

        case UPDATE_POST_SUCCESS:
            return {
                isRunning: false,
                isFinished: true,
                hasError: false,
                response: action.payload
            };
        break;

        case UPDATE_POST_ERROR:
            return {
                isRunning: false,
                isFinished: false,
                hasError: true,
                response: null,
                statusCode: action.statusCode
            };

        case UPDATE_POST_INVALIDATE:
            return initialState;
        break;

        default:
            return state;
    }
};

There is no problem with the approach above, but I find the the "process stages" confusing. Therefor my idea is to shorten it to:

// IMPORTED FROM SEPARATE FILE
const stage = {
  INITIAL: "INITIAL",
  RUNNING: "RUNNING",
  FINISHED: "FINISHED",
  ERROR: "ERROR"
};

const initialState = {
    stage: stage.INITIAL,
    response: null
};
export const updatePostReducer = (state = initialState, action=null) => {
    switch (action.type) {
        case UPDATE_POST:
            return {
                stage: stage.RUNNING,
                response: null
            };
        case UPDATE_POST_SUCCESS:
            return {
                stage: stage.FINISHED,
                response: action.payload
            };
        case UPDATE_POST_ERROR:
            return {
                stage: stage.ERROR,
                response: null,
                statusCode: action.statusCode
            };
        case UPDATE_POST_INVALIDATE:
            return initialState;

        default:
            return state;
    }
};

I see 2 advantages using the latter approach:

  1. It's cleaner
  2. It's possible to use switch() instead of filthy if-elses

The disadvantages are:

  1. This requires to import stages.js everywhere.
  2. There is no fine-grained control of the stages

My question is: Since there is no fine-grained control of the stages, will this cause problems in some scenarios? For example, could there be occasions where it is needed to have hasError=true, isRunning=true at the same time?

Upvotes: 1

Views: 36

Answers (1)

Alp
Alp

Reputation: 29739

The question you pose is difficult to answer without knowing the use cases of your application. But if it's not a very simple one, i'd state that you could run into problems sooner or later with that kind of simplification.

I also see some problems with non-initialized values like statusCode. Which is not updated for the other states by the way. If the code is updated when an error occurs, it's still the same after a successful retry.

I would propose another way, being even more explicit with combineReducers().

Here is how:

const isRunningReducer = (state=false, action=null) => {
    switch (action.type) {
        case UPDATE_POST:
            return true;
        case UPDATE_POST_SUCCESS:
            return false;
        case UPDATE_POST_ERROR:
            return false;
        case UPDATE_POST_INVALIDATE:
            return false;
        default:
            return state;
    }
};

const isFinishedReducer = (state=false, action=null) => {
    switch (action.type) {
        case UPDATE_POST:
            return false;
        case UPDATE_POST_SUCCESS:
            return true;
        case UPDATE_POST_ERROR:
            return false;
        case UPDATE_POST_INVALIDATE:
            return false;
        default:
            return state;
    }
};

const hasErrorReducer = (state=false, action=null) => {
    switch (action.type) {
        case UPDATE_POST:
            return false;
        case UPDATE_POST_SUCCESS:
            return false;
        case UPDATE_POST_ERROR:
            return true;
        case UPDATE_POST_INVALIDATE:
            return false;
        default:
            return state;
    }
};

const statusCodeReducer = (state=null, action=null) => {
    switch (action.type) {
        case UPDATE_POST:
            return null;
        case UPDATE_POST_SUCCESS:
            return action.statusCode;
        case UPDATE_POST_ERROR:
            return action.statusCode;
        case UPDATE_POST_INVALIDATE:
            return null;
        default:
            return state;
    }
};

const responseReducer = (state=null, action=null) => {
    switch (action.type) {
        case UPDATE_POST:
            return null;
        case UPDATE_POST_SUCCESS:
            return action.payload;
        case UPDATE_POST_ERROR:
            return null;
        case UPDATE_POST_INVALIDATE:
            return null;
        default:
            return state;
    }
};

export const updatePostReducer = combineReducers({
    isRunningReducer,
    isFinishedReducer,
    hasErrorReducer,
    statusCodeReducer,
    responseReducer,
});

Disadvantages:

  • more verbose
  • repetitive

Advantages:

  • very simple and understandable reducers
  • less error-prone
  • easily extendible
  • easy to write generator code so that standard reducers like "isRunning", "isFinished", etc. can be imported

All of this can be made even more elegant with an own createReducer() method as described here: Reducing Boilerplate - Generating Reducers

Upvotes: 1

Related Questions