Search code examples
reactjstypescriptreduxreact-reduxredux-thunk

How do I avoid using separate _PENDING _FULFILLED and _REJECTED actions with redux thunk?


I am writing my actions and reducers with thunks that dispatch _PENDING, _FULFILLED, and _REJECTED actions. However, I am wanting a better solution to avoid the boilerplate. I am migrating to Typescript which doubles this boilerplate by requiring an interface for each _PENDING, _FULFILLED, and _REJECTED action. It is just getting out of hand. Is there a way to get the same/similar functionality of my code without having three action types per thunk?

localUserReducer.js

const initialState = {
  fetching: false,
  fetched: false,
  user: undefined,
  errors: undefined,
};

export default function (state = initialState, action) {
  switch (action.type) {
    case 'GET_USER_PENDING':
      return {
        ...state,
        fetching: true,
      };
    case 'GET_USER_FULFILLED':
      return {
        ...state,
        fetching: false,
        fetched: true,
        user: action.payload,
      };
    case 'GET_USER_REJECTED':
      return {
        ...state,
        fetching: false,
        errors: action.payload,
      };
    default:
      return state;
  }
}

localUserActions.js

import axios from 'axios';

export const getUser = () => async (dispatch) => {
  dispatch({ type: 'GET_USER_PENDING' });
  try {
    const { data } = await axios.get('/api/auth/local/current');
    dispatch({ type: 'GET_USER_FULFILLED', payload: data });
  } catch (err) {
    dispatch({ type: 'GET_USER_REJECTED', payload: err.response.data });
  }
};

I may have a huge misunderstand of redux-thunk as I am a newbie. I don't understand how I can send _REJECTED actions if I use the implementation of Typescript and redux-thunk documented here: https://redux.js.org/recipes/usage-with-typescript#usage-with-redux-thunk


Solution

  • There is a way to get the similar functionality without having three action types per thunk, but it will have some impact on the rendering logic.

    I'd recommend pushing the transient aspect of the async calls down to the data. So rather than marking your actions as _PENDING, _FULFILLED, and _REJECTED, mark your data that way, and have a single action.

    localUser.js (new file for the user type)

    // Use a discriminated union here to keep inapplicable states isolated
    type User =
      { status: 'ABSENT' } |
      { status: 'PENDING' } |
      { status: 'FULLFILLED', data: { fullName: string } } |
      { status: 'REJECTED', error: string };
    
    // a couple of constructors for the fullfilled and rejected data
    function dataFulFilled(data: { fullName: string }) {
      return ({ status: 'FULLFILLED', data });
    }
    
    function dataRejected(error: string) {
      return ({ status: 'REJECTED', error });
    }
    

    localUserReducer.js

    const initialState: { user: User } = { user: { status: 'ABSENT' } };
    
    export default function (state = initialState, action): { user: User } {
      switch (action.type) {
        case 'USER_CHANGED':
          return {
            ...state,
            user: action.payload
          };
        default:
          return state;
      }
    }
    

    localUserActions.js

    import axios from 'axios';
    
    export const getUser = () => async (dispatch) => {
      dispatch({ type: 'USER_CHANGED', payload: { status: 'PENDING' } });
      try {
        const { data } = await axios.get('/api/auth/local/current');
        dispatch({ type: 'USER_CHANGED', payload: dataFulFilled(data) });
      } catch (err) {
        dispatch({ type: 'USER_CHANGED', payload: dataRejected(err.response.data) });
      }
    };
    

    This will also remove the need for the multiple boolean fields (fetching and fetched) and isolate the various data states from accidental modification.

    The changes to the render logic will be necessary, but will likely be an improvement. Rather than combinations of nested if-else statements using the booleans, a single switch can be used to handle the four cases of the data state.

    Then you can invoke something like this from your render function...

    function userElement(user: User) {
      switch (user.status) {
        case 'ABSENT':
          return <></>;
        case 'PENDING':
          return <div>Fetching user information...Please be patient...</div>;
        case 'FULLFILLED':
          return <div>{user.data.fullName}</div>;
        case 'REJECTED':
          return <h1>The error is: {user.error}</h1>
      }
    }
    

    I hope that helps. Good luck!