useApiService.ts 2.73 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
import { useEffect, useReducer, Reducer } from 'react';

import apiService, { FetchOptions, APIError } from 'src/services/api-service';

import { LoadingState } from 'src/shared/types/loading-state';

type Params = FetchOptions & {
  endpoint: string;
  isAbortable?: boolean;
  skip?: boolean;
};

type StateBeforeRequest = {
  loadingState: LoadingState.NOT_REQUESTED;
  data: null;
  error: null;
};

type StateAtLoading = {
  loadingState: LoadingState.LOADING;
  data: null;
  error: null;
};

type StateAtSuccess<T> = {
  loadingState: LoadingState.SUCCESS;
  data: T;
  error: null;
};

type StateAtError = {
  loadingState: LoadingState.ERROR;
  data: null;
  error: APIError;
};

type State<T> =
  | StateBeforeRequest
  | StateAtLoading
  | StateAtSuccess<T>
  | StateAtError;

type LoadingAction = {
  type: 'loading';
};

type SuccessAction<T> = {
  type: 'success';
  payload: T;
};

type ErrorAction = {
  type: 'error';
  payload: APIError;
};

type Action<T> = LoadingAction | SuccessAction<T> | ErrorAction;

const initialState: StateBeforeRequest = {
  loadingState: LoadingState.NOT_REQUESTED,
  data: null,
  error: null
};

const reducer = <T>(state: State<T>, action: Action<T>): State<T> => {
  switch (action.type) {
    case 'loading':
      return initialState;
    case 'success':
      return {
        loadingState: LoadingState.SUCCESS,
        data: action.payload,
        error: null
      };
    case 'error':
      return {
        loadingState: LoadingState.ERROR,
        data: null,
        error: action.payload
      };
    default:
      return state;
  }
};

const useApiService = <T>(params: Params): State<T> => {
  const [state, dispatch] = useReducer<Reducer<State<T>, Action<T>>>(
    reducer,
    initialState
  );

  useEffect(() => {
    if (params.skip) {
      return;
    }
    let canUpdate = true;
    dispatch({ type: 'loading' });
    const { endpoint, isAbortable, ...fetchOptions } = params;
    const abortController = new AbortController();
    if (isAbortable) {
      fetchOptions.signal = abortController.signal;
    }

    apiService
      .fetch(endpoint, fetchOptions)
      .then((data) => {
        // notice that if the request has been aborted and the abort error caught in api service,
        // the promise will resolve with empty data; but, since canUpdate will be false,
        // the state will not get updated with empty data
        if (canUpdate) {
          dispatch({ type: 'success', payload: data });
        }
      })
      .catch((error) => {
        dispatch({ type: 'error', payload: error });
      });

    return () => {
      canUpdate = false;
      isAbortable && abortController.abort();
    };
  }, [params.endpoint, params.host, params.skip]);

  return state;
};

export default useApiService;