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>(
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();
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;
33 const withLoading = useCallback<WithLoadingByKey>((key: string, maybeWrappedPromise) => {
34 if (!maybeWrappedPromise) {
35 setLoading((prev) => ({ ...prev, [key]: false }));
36 return Promise.resolve();
39 const promise = unwrapPromise(maybeWrappedPromise);
41 const counterNext = getCurrentCounterRef(key) + 1;
42 counterRefByKey.current = { ...counterRefByKey.current, [key]: counterNext };
44 setLoading((prev) => ({ ...prev, [key]: true }));
48 // Ensure that the latest promise is setting the new state
49 if (getCurrentCounterRef(key) !== counterNext) {
53 if (!unmountedRef.current) {
54 setLoading((prev) => ({ ...prev, [key]: false }));
60 if (getCurrentCounterRef(key) !== counterNext) {
64 if (!unmountedRef.current) {
65 setLoading((prev) => ({ ...prev, [key]: false }));
72 const setManualLoading = (key: string, value: boolean) => {
73 setLoading((prev) => ({ ...prev, [key]: value }));
77 unmountedRef.current = false;
79 unmountedRef.current = true;
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(
93 return withLoadingByKey(SINGLE_LOADING_KEY, args);
98 const setLoading = useCallback(
99 (loading: boolean) => {
100 return setLoadingByKey(SINGLE_LOADING_KEY, loading);
105 return [loadingByKey[SINGLE_LOADING_KEY] as boolean, withLoading, setLoading];
108 export default useLoading;