|
| 1 | +/* |
| 2 | + * Copyright © 2024, RTE (http://www.rte-france.com) |
| 3 | + * This Source Code Form is subject to the terms of the Mozilla Public |
| 4 | + * License, v. 2.0. If a copy of the MPL was not distributed with this |
| 5 | + * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| 6 | + */ |
| 7 | + |
| 8 | +import { useCallback, useReducer } from 'react'; |
| 9 | +import { FetchStatus, FetchStatusType } from '@gridsuite/commons-ui'; |
| 10 | + |
| 11 | +export enum ActionType { |
| 12 | + START = 'START', |
| 13 | + ERROR = 'ERROR', |
| 14 | + SUCCESS = 'SUCCESS', |
| 15 | + ADD_ERROR = 'ADD_ERROR', // Use by multipleDeferredFetch when one request respond with error |
| 16 | + ADD_SUCCESS = 'ADD_SUCCESS', // Use by multipleDeferredFetch when one request respond with success |
| 17 | +} |
| 18 | + |
| 19 | +type FetchAction = |
| 20 | + | { |
| 21 | + type: ActionType.START; |
| 22 | + } |
| 23 | + | { |
| 24 | + type: ActionType.ERROR; |
| 25 | + payload: unknown; |
| 26 | + } |
| 27 | + | { |
| 28 | + type: ActionType.SUCCESS; |
| 29 | + payload: unknown; |
| 30 | + } |
| 31 | + | { |
| 32 | + type: ActionType.ADD_ERROR; |
| 33 | + } |
| 34 | + | { |
| 35 | + type: ActionType.ADD_SUCCESS; |
| 36 | + }; |
| 37 | + |
| 38 | +type FetchState = { |
| 39 | + status: FetchStatusType; |
| 40 | + data: unknown; |
| 41 | + errorMessage: unknown; |
| 42 | +}; |
| 43 | + |
| 44 | +const initialState: FetchState = { |
| 45 | + status: FetchStatus.IDLE, |
| 46 | + errorMessage: '', |
| 47 | + data: null, |
| 48 | +}; |
| 49 | + |
| 50 | +/** |
| 51 | + * This custom hook manage a fetch workflow and return a unique callback to defer process execution when needed. |
| 52 | + * It also returns a unique state which contains fetch status, results and error message if it failed. |
| 53 | + * @param {function} fetchFunction the fetch function to call |
| 54 | + * @param {Object} params Params of the fetch function. WARNING: Must respect order here |
| 55 | + * @param {function} onSuccess callback to call on request success |
| 56 | + * @param {function} errorToString callback to translate HTTPCode to string error messages |
| 57 | + * @param {function} onError callback to call if request failed |
| 58 | + * @param {boolean} hasResult Configure if fetchFunction return results or only HTTP request response |
| 59 | + * @returns {function} fetchCallback The callback to call to execute the request. |
| 60 | + * It accepts params as argument which must follow fetch function params. |
| 61 | + * @returns {state} state complete state of the request |
| 62 | + * {Enum} state.status Status of the request |
| 63 | + * {String} state.errorMessage error message of the request |
| 64 | + * {Object} state.data The JSON results of the request (see hasResult) |
| 65 | + */ |
| 66 | +export default function useDeferredFetch<TArgs extends any[]>( |
| 67 | + fetchFunction: (...args: TArgs) => Promise<void>, |
| 68 | + onSuccess: ((data: unknown | null, args: TArgs) => void) | undefined, |
| 69 | + errorToString: ((status: unknown) => string) | undefined = undefined, |
| 70 | + onError: ((errorMessage: unknown | null, paramsOnError: TArgs) => void) | undefined = undefined, |
| 71 | + hasResult: boolean = true |
| 72 | +): [(...args: TArgs) => void, FetchState] { |
| 73 | + const [state, dispatch] = useReducer((lastState: FetchState, action: FetchAction) => { |
| 74 | + switch (action.type) { |
| 75 | + case ActionType.START: |
| 76 | + return { ...initialState, status: FetchStatus.FETCHING }; |
| 77 | + case ActionType.SUCCESS: |
| 78 | + return { |
| 79 | + ...initialState, |
| 80 | + status: FetchStatus.FETCH_SUCCESS, |
| 81 | + data: action.payload, |
| 82 | + }; |
| 83 | + case ActionType.ERROR: |
| 84 | + return { |
| 85 | + ...initialState, |
| 86 | + status: FetchStatus.FETCH_ERROR, |
| 87 | + errorMessage: action.payload, |
| 88 | + }; |
| 89 | + default: |
| 90 | + return lastState; |
| 91 | + } |
| 92 | + }, initialState); |
| 93 | + |
| 94 | + const handleError = useCallback( |
| 95 | + (error: any, paramsOnError: TArgs) => { |
| 96 | + const defaultErrorMessage = error.message; |
| 97 | + let errorMessage = defaultErrorMessage; |
| 98 | + if (error && errorToString) { |
| 99 | + const providedErrorMessage = errorToString(error.status); |
| 100 | + if (providedErrorMessage && providedErrorMessage !== '') { |
| 101 | + errorMessage = providedErrorMessage; |
| 102 | + } |
| 103 | + } |
| 104 | + dispatch({ |
| 105 | + type: ActionType.ERROR, |
| 106 | + payload: errorMessage, |
| 107 | + }); |
| 108 | + if (onError) { |
| 109 | + onError(errorMessage, paramsOnError); |
| 110 | + } |
| 111 | + }, |
| 112 | + [errorToString, onError] |
| 113 | + ); |
| 114 | + |
| 115 | + const fetchData = useCallback( |
| 116 | + async (...args: TArgs) => { |
| 117 | + dispatch({ type: ActionType.START }); |
| 118 | + try { |
| 119 | + // Params resolution |
| 120 | + const response = await fetchFunction(...args); |
| 121 | + |
| 122 | + if (hasResult) { |
| 123 | + dispatch({ |
| 124 | + type: ActionType.SUCCESS, |
| 125 | + payload: response, |
| 126 | + }); |
| 127 | + if (onSuccess) { |
| 128 | + onSuccess(response, args); |
| 129 | + } |
| 130 | + } else { |
| 131 | + dispatch({ |
| 132 | + type: ActionType.SUCCESS, |
| 133 | + payload: null, |
| 134 | + }); |
| 135 | + if (onSuccess) { |
| 136 | + onSuccess(null, args); |
| 137 | + } |
| 138 | + } |
| 139 | + } catch (error: any) { |
| 140 | + if (!error.status) { |
| 141 | + // an http error |
| 142 | + handleError(null, args); |
| 143 | + throw error; |
| 144 | + } else { |
| 145 | + handleError(error, args); |
| 146 | + } |
| 147 | + } |
| 148 | + }, |
| 149 | + [fetchFunction, onSuccess, handleError, hasResult] |
| 150 | + ); |
| 151 | + |
| 152 | + const fetchCallback = useCallback( |
| 153 | + (...args: TArgs) => { |
| 154 | + fetchData(...args); |
| 155 | + }, |
| 156 | + [fetchData] |
| 157 | + ); |
| 158 | + |
| 159 | + return [fetchCallback, state]; |
| 160 | +} |
0 commit comments