From 8bcc6105035131caf128890c1480c9cd7cd5101f Mon Sep 17 00:00:00 2001 From: Sergio Betanzos Date: Wed, 23 Jun 2021 09:41:20 +0200 Subject: [PATCH] F OpenNebula/one#5422: Add reducer to fetch hooks --- src/fireedge/src/client/hooks/useFetch.js | 124 +++++++++++-------- src/fireedge/src/client/hooks/useFetchAll.js | 120 +++++++++++------- 2 files changed, 151 insertions(+), 93 deletions(-) diff --git a/src/fireedge/src/client/hooks/useFetch.js b/src/fireedge/src/client/hooks/useFetch.js index 75a2497b6b..13a7031a5d 100644 --- a/src/fireedge/src/client/hooks/useFetch.js +++ b/src/fireedge/src/client/hooks/useFetch.js @@ -1,64 +1,90 @@ -import { useState, useCallback, useEffect, useRef } from 'react' -import { debounce } from '@material-ui/core' +import { useReducer, useCallback, useEffect, useRef } from 'react' import { fakeDelay } from 'client/utils' -import { console } from 'window-or-global' -const useRequest = request => { - const [data, setData] = useState(undefined) - const [loading, setLoading] = useState(false) - const [reloading, setReloading] = useState(false) - const [error, setError] = useState(false) - const isMounted = useRef(true) +const STATUS = { + INIT: 'INIT', + PENDING: 'PENDING', + ERROR: 'ERROR', + FETCHED: 'FETCHED' +} - useEffect(() => () => { isMounted.current = false }, []) +const ACTIONS = { + REQUEST: 'REQUEST', + SUCCESS: 'SUCCESS', + FAILURE: 'FAILURE' +} - const doFetch = useCallback( - debounce(payload => - request(payload) - .then(response => { - if (isMounted.current) { - if (response !== undefined) { - setData(response) - setError(false) - } else setError(true) - } - }) - .catch(e => { - console.log('error', e) - if (isMounted.current) { - setData(undefined) - setError(true) - } - }) - .finally(() => { - if (isMounted.current) { - setLoading(false) - setReloading(false) - } - }) - ), [isMounted]) +const INITIAL_STATE = { + status: STATUS.INIT, + error: undefined, + data: undefined, + loading: false, + reloading: false +} + +const fetchReducer = (state, action) => { + const { type, payload, reload = false } = action + + return { + [ACTIONS.REQUEST]: { + ...INITIAL_STATE, + status: STATUS.PENDING, + [reload ? 'reloading' : 'loading']: true + }, + [ACTIONS.SUCCESS]: { + ...INITIAL_STATE, + status: STATUS.FETCHED, + data: payload + }, + [ACTIONS.FAILURE]: { + ...INITIAL_STATE, + status: STATUS.ERROR, + error: payload + } + }[type] ?? state +} + +const useFetch = request => { + const cancelRequest = useRef(false) + const [state, dispatch] = useReducer(fetchReducer, INITIAL_STATE) + + useEffect(() => { + return () => { + cancelRequest.current = true + } + }, []) + + const doFetch = useCallback(async (payload, reload = false) => { + dispatch({ type: ACTIONS.REQUEST, reload }) + + try { + const response = await request(payload) + + if (response === undefined) throw response + if (cancelRequest.current) return + + dispatch({ type: ACTIONS.SUCCESS, payload: response }) + } catch (error) { + if (cancelRequest.current) return + + const errorMessage = typeof error === 'string' ? error : error?.message + + dispatch({ type: ACTIONS.FAILURE, payload: errorMessage }) + } + }, [request]) const fetchRequest = useCallback((payload, options = {}) => { const { reload = false, delay = 0 } = options + if (!(Number.isInteger(delay) && delay >= 0)) { console.error(` Delay must be a number >= 0! If you're using it as a function, it must also return a number >= 0.`) } - if (isMounted.current) { - reload ? setReloading(true) : setLoading(true) - } + fakeDelay(delay).then(() => doFetch(payload, reload)) + }, [request]) - fakeDelay(delay).then(() => doFetch(payload)) - }, [isMounted]) - - return { - data, - fetchRequest, - loading, - reloading, - error - } + return { ...state, fetchRequest } } -export default useRequest +export default useFetch diff --git a/src/fireedge/src/client/hooks/useFetchAll.js b/src/fireedge/src/client/hooks/useFetchAll.js index 7dddc3ac31..c8a0e4af2a 100644 --- a/src/fireedge/src/client/hooks/useFetchAll.js +++ b/src/fireedge/src/client/hooks/useFetchAll.js @@ -1,58 +1,90 @@ -import { useState, useCallback, useEffect, useRef } from 'react' -import { debounce } from '@material-ui/core' +import { useReducer, useCallback, useEffect, useRef } from 'react' import { fakeDelay } from 'client/utils' -const useRequestAll = () => { - const [data, setData] = useState(undefined) - const [loading, setLoading] = useState(false) - const [reloading, setReloading] = useState(false) - const [error, setError] = useState(false) - const isMounted = useRef(true) +const STATUS = { + INIT: 'INIT', + PENDING: 'PENDING', + ERROR: 'ERROR', + FETCHED: 'FETCHED' +} - useEffect(() => () => { isMounted.current = false }, []) +const ACTIONS = { + REQUEST: 'REQUEST', + SUCCESS: 'SUCCESS', + FAILURE: 'FAILURE' +} - const doFetchs = useCallback( - debounce((requests, onError) => - Promise - .all(requests) - .then(response => { - if (isMounted.current) { - if (response !== undefined) { - setData(response) - setError(false) - } else setError(true) +const INITIAL_STATE = { + status: STATUS.INIT, + error: undefined, + data: undefined, + loading: false, + reloading: false +} - isMounted.current && setLoading(false) - isMounted.current && setReloading(false) - } - // }).catch(onError) - }).catch(err => { - setError(true) - onError?.(err) - }) - ), [isMounted]) +const fetchReducer = (state, action) => { + const { type, payload, reload = false } = action + + return { + [ACTIONS.REQUEST]: { + ...INITIAL_STATE, + status: STATUS.PENDING, + [reload ? 'reloading' : 'loading']: true + }, + [ACTIONS.SUCCESS]: { + ...INITIAL_STATE, + status: STATUS.FETCHED, + data: payload + }, + [ACTIONS.FAILURE]: { + ...INITIAL_STATE, + status: STATUS.ERROR, + error: payload + } + }[type] ?? state +} + +const useFetchAll = () => { + const cancelRequest = useRef(false) + const [state, dispatch] = useReducer(fetchReducer, INITIAL_STATE) + + useEffect(() => { + return () => { + cancelRequest.current = true + } + }, []) + + const doFetches = useCallback(async (requests, reload = false) => { + dispatch({ type: ACTIONS.REQUEST, reload }) + + try { + const response = Promise.all(requests) + + if (response === undefined) throw response + if (cancelRequest.current) return + + dispatch({ type: ACTIONS.SUCCESS, payload: response }) + } catch (error) { + if (cancelRequest.current) return + + const errorMessage = typeof error === 'string' ? error : error?.message + + dispatch({ type: ACTIONS.FAILURE, payload: errorMessage }) + } + }, []) + + const fetchRequest = useCallback((requests, options = {}) => { + const { reload = false, delay = 0 } = options - const fetchRequestAll = useCallback((requests, options = {}) => { - const { reload = false, delay = 0, onError } = options if (!(Number.isInteger(delay) && delay >= 0)) { console.error(` Delay must be a number >= 0! If you're using it as a function, it must also return a number >= 0.`) } - if (isMounted.current) { - reload ? setReloading(true) : setLoading(true) - } + fakeDelay(delay).then(() => doFetches(requests, reload)) + }, []) - fakeDelay(delay).then(() => doFetchs(requests, onError)) - }, [isMounted]) - - return { - data, - fetchRequestAll, - loading, - reloading, - error - } + return { ...state, fetchRequest } } -export default useRequestAll +export default useFetchAll