Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / hooks / useLoading.ts
blobbd832ae4c7d280428faae98f1566c1dc017e3865
1 import { useCallback, useEffect, useRef, useState } from 'react';
3 import type { SimpleMap } from '@proton/shared/lib/interfaces';
4 import isFunction from '@proton/utils/isFunction';
6 export type WithLoading = <T>(promise: undefined | Promise<T | void> | (() => Promise<T | void>)) => Promise<T | void>;
7 export type WithLoadingByKey = <T>(
8     key: string,
9     promise: undefined | Promise<T | void> | (() => Promise<T | void>)
10 ) => Promise<T | void>;
12 function unwrapPromise<T>(maybeWrappedPromise: Promise<T | void> | (() => Promise<T | void>)): Promise<T | void> {
13     if (isFunction(maybeWrappedPromise)) {
14         return maybeWrappedPromise();
15     }
17     return maybeWrappedPromise;
20 export type LoadingByKey = SimpleMap<boolean>;
22 export const useLoadingByKey = (
23     initialState: LoadingByKey = {}
24 ): [LoadingByKey, WithLoadingByKey, (key: string, loading: boolean) => void] => {
25     const [loading, setLoading] = useState<LoadingByKey>(initialState);
26     const unmountedRef = useRef(false);
27     const counterRefByKey = useRef<SimpleMap<number>>({});
29     const getCurrentCounterRef = (key: string) => {
30         return counterRefByKey.current[key] ?? 0;
31     };
33     const withLoading = useCallback<WithLoadingByKey>((key: string, maybeWrappedPromise) => {
34         if (!maybeWrappedPromise) {
35             setLoading((prev) => ({ ...prev, [key]: false }));
36             return Promise.resolve();
37         }
39         const promise = unwrapPromise(maybeWrappedPromise);
41         const counterNext = getCurrentCounterRef(key) + 1;
42         counterRefByKey.current = { ...counterRefByKey.current, [key]: counterNext };
44         setLoading((prev) => ({ ...prev, [key]: true }));
46         return promise
47             .then((result) => {
48                 // Ensure that the latest promise is setting the new state
49                 if (getCurrentCounterRef(key) !== counterNext) {
50                     return;
51                 }
53                 if (!unmountedRef.current) {
54                     setLoading((prev) => ({ ...prev, [key]: false }));
55                 }
57                 return result;
58             })
59             .catch((e) => {
60                 if (getCurrentCounterRef(key) !== counterNext) {
61                     return;
62                 }
64                 if (!unmountedRef.current) {
65                     setLoading((prev) => ({ ...prev, [key]: false }));
66                 }
68                 throw e;
69             });
70     }, []);
72     const setManualLoading = (key: string, value: boolean) => {
73         setLoading((prev) => ({ ...prev, [key]: value }));
74     };
76     useEffect(() => {
77         unmountedRef.current = false;
78         return () => {
79             unmountedRef.current = true;
80         };
81     }, []);
83     return [loading, withLoading, setManualLoading];
86 const SINGLE_LOADING_KEY = 'main';
88 const useLoading = (initialState = false): [boolean, WithLoading, (loading: boolean) => void] => {
89     const [loadingByKey, withLoadingByKey, setLoadingByKey] = useLoadingByKey({ [SINGLE_LOADING_KEY]: initialState });
91     const withLoading: WithLoading = useCallback(
92         (args) => {
93             return withLoadingByKey(SINGLE_LOADING_KEY, args);
94         },
95         [withLoadingByKey]
96     );
98     const setLoading = useCallback(
99         (loading: boolean) => {
100             return setLoadingByKey(SINGLE_LOADING_KEY, loading);
101         },
102         [setLoadingByKey]
103     );
105     return [loadingByKey[SINGLE_LOADING_KEY] as boolean, withLoading, setLoading];
108 export default useLoading;