All files / src/hooks usePromise.js

100% Statements 33/33
100% Branches 12/12
100% Functions 14/14
100% Lines 29/29
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                                          37x 37x 37x   37x     12x     12x 7x 7x   5x       12x 12x 11x 1x 12x       37x 8x 1x 1x           37x 10x 10x 10x     37x                                       3x 3x   3x   3x 1x     3x    
import { useState, useCallback, useEffect, useRef } from "react"
 
/**
 * usePromiseCallback Hook
 *
 * Hook dedicated to offer a sync way of accessing a promise state.
 *
 * Keeps the last result until new results are loaded, to avoid UI flashes.
 *
 * Supports a delayMS param to customise the loading threshold, to avoid UI flashes.
 *
 * Only processes the last promise call.
 *
 * @param {Promise} promise - The promise that would be wrapped
 * @param {any[]} deps - extra deps used to re-create the callback
 * @param {number} delayMS - The delay in ms to switch the loading state to true
 *
 * @example
 * const [callback, result, loading, err] = usePromiseCallback(Promise.resolve, [], 500)
 */
export function usePromiseCallback(promise, deps = [], delayMS = 0) {
    const [state, setState] = useState({})
    const timeoutRef = useRef(0)
    const callID = useRef(0)
 
    const callback = useCallback((...fnArgs) => {
        // set loading function helper
        // keep the last result so we avoid UI flashes
        const setLoading = state => ({ result: state.result, loading: true })
 
        // if a delay is set, create a timeout to set the loading state
        if (delayMS) {
            clearTimeout(timeoutRef.current)
            timeoutRef.current = setTimeout(() => setState(setLoading), delayMS)
        } else {
            setState(setLoading)
        }
 
        // only process if currentCallID is valid
        const currentCallID = ++callID.current
        return promise(...fnArgs)
            .then(result => currentCallID === callID.current && setState({ result }))
            .catch(err => currentCallID === callID.current && setState({ err }))
            .finally(() => currentCallID === callID.current && clearTimeout(timeoutRef.current))
    }, deps)
 
    // clear timeout on umount and reset callID
    useEffect(
        () => () => {
            callID.current = 0
            clearTimeout(timeoutRef.current)
        },
        []
    )
 
    // clear timeout on deps change
    useEffect(() => {
        callID.current++
        clearTimeout(timeoutRef.current)
        setState(({ result }) => ({ result }))
    }, [...deps, delayMS])
 
    return [callback, state.result, state.loading, state.err]
}
 
/**
 * usePromise Hook
 *
 * Wrapped {@link module:hooks.usePromiseCallback|usePromiseCallback}.
 *
 * It gets triggered on init and every time the args change by default.
 *
 * Useful to fetch page data.
 *
 * @param {Promise} promise - The promise that would be wrapped
 * @param {any[]} deps - deps to call the function with
 * @param {number} delayMS - The delay in ms to switch the loading state to true
 *
 * @example
 * const [callback, result, loading, err] = usePromise(Promise.resolve, ["hello"], 500)
 */
export function usePromise(promise, deps = [], delayMS = 0) {
    const promiseCallback = usePromiseCallback(promise, deps, delayMS)
    const [cb, ...props] = promiseCallback
 
    const callback = useCallback(() => cb(...deps), [cb])
 
    useEffect(() => {
        callback()
    }, [callback])
 
    return [callback, ...props]
}