All files / blong-browser/src/hooks useAsync.ts

49.2% Statements 31/63
100% Branches 1/1
0% Functions 0/1
49.2% Lines 31/63

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 641x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                                                 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
/**
 * useAsync — async loader with status tracking.
 */
import {useEffect, useRef, useState, type DependencyList} from 'react';
import type {IBlongError} from '../types/action.js';
 
export type AsyncStatus = 'idle' | 'loading' | 'success' | 'error';
 
export interface IUseAsyncResult<T> {
    status: AsyncStatus;
    data?: T;
    error?: IBlongError | Error;
    loading: boolean;
    reload: () => void;
}

export function useAsync<T>(
    asyncFn: () => Promise<T>,
    deps: DependencyList = [],
): IUseAsyncResult<T> {
    const [status, setStatus] = useState<AsyncStatus>('idle');
    const [data, setData] = useState<T | undefined>();
    const [error, setError] = useState<IBlongError | Error | undefined>();
    const [reloadKey, setReloadKey] = useState(0);
    const asyncFnRef = useRef(asyncFn);
    asyncFnRef.current = asyncFn;

    useEffect(() => {
        let cancelled = false;
        // eslint-disable-next-line @eslint-react/set-state-in-effect
        setStatus('loading');
        // eslint-disable-next-line @eslint-react/set-state-in-effect
        setError(undefined);

        asyncFnRef
            .current()
            .then(result => {
                if (!cancelled) {
                    setData(result);
                    setStatus('success');
                }
            })
            .catch(err => {
                if (!cancelled) {
                    setError(err as IBlongError | Error);
                    setStatus('error');
                }
            });
 
        return () => {
            cancelled = true;
        };
        // eslint-disable-next-line @eslint-react/exhaustive-deps -- spread deps from caller are intentional
    }, [...deps, reloadKey]);
 
    return {
        status,
        data,
        error,
        loading: status === 'loading' || status === 'idle',
        reload: () => setReloadKey(k => k + 1),
    };
}