From 06778bc0eca4b2e7b93a558863c6f88e731cca76 Mon Sep 17 00:00:00 2001 From: naumov Date: Wed, 25 Mar 2026 22:40:30 +0100 Subject: [PATCH 1/6] dbeaver/pro#8746 add useQuery hook --- webapp/packages/core-blocks/src/index.ts | 1 + webapp/packages/core-blocks/src/useQuery.ts | 42 +++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 webapp/packages/core-blocks/src/useQuery.ts diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index a94c34875f..5be6c7f20e 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -262,3 +262,4 @@ export * from './ObjectPropertyInfo/evaluate.js'; export * from './ObjectPropertyInfo/getObjectPropertyDefaults.js'; export * from './useVisible.js'; export * from './SAVED_VALUE_INDICATOR.js'; +export * from './useQuery.js'; diff --git a/webapp/packages/core-blocks/src/useQuery.ts b/webapp/packages/core-blocks/src/useQuery.ts new file mode 100644 index 0000000000..c437353641 --- /dev/null +++ b/webapp/packages/core-blocks/src/useQuery.ts @@ -0,0 +1,42 @@ +import { useState, useEffect, useRef, useLayoutEffect } from 'react'; + +interface QueryState { + data: T | null; + loading: boolean; + error: Error | null; +} + +export function useQuery(queryFn: () => Promise, deps: any[] = []): QueryState { + const [state, setState] = useState>({ + data: null, + loading: true, // start as true + error: null, + }); + + const abortRef = useRef(null); + const queryFnRef = useRef(queryFn); + + useLayoutEffect(() => { + queryFnRef.current = queryFn; + }, [queryFn]); + + useEffect(() => { + abortRef.current?.abort(); + abortRef.current = new AbortController(); + + queryFnRef + .current() + .then(data => setState({ data, loading: false, error: null })) + .catch(error => { + if (error.name === 'AbortError') { + return; + } + + setState({ data: null, loading: false, error }); + }); + + return () => abortRef.current?.abort(); + }, deps); + + return state; +} From 1c1635033a61d28488b0bbfbc34dd5320510c97f Mon Sep 17 00:00:00 2001 From: naumov Date: Thu, 26 Mar 2026 18:32:11 +0100 Subject: [PATCH 2/6] dbeaver/pro#8746 add language tokens --- webapp/packages/core-blocks/src/useQuery.ts | 11 ++++++++++- webapp/packages/core-localization/src/locales/en.ts | 2 ++ webapp/packages/core-localization/src/locales/fr.ts | 2 ++ webapp/packages/core-localization/src/locales/it.ts | 2 ++ webapp/packages/core-localization/src/locales/ru.ts | 2 ++ webapp/packages/core-localization/src/locales/vi.ts | 2 ++ webapp/packages/core-localization/src/locales/zh.ts | 2 ++ 7 files changed, 22 insertions(+), 1 deletion(-) diff --git a/webapp/packages/core-blocks/src/useQuery.ts b/webapp/packages/core-blocks/src/useQuery.ts index c437353641..e1e4090511 100644 --- a/webapp/packages/core-blocks/src/useQuery.ts +++ b/webapp/packages/core-blocks/src/useQuery.ts @@ -1,3 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + import { useState, useEffect, useRef, useLayoutEffect } from 'react'; interface QueryState { @@ -6,10 +14,11 @@ interface QueryState { error: Error | null; } +/** We can use this if we don’t have ILoadableState and we fetch data that is not supposed to be kept in the resource */ export function useQuery(queryFn: () => Promise, deps: any[] = []): QueryState { const [state, setState] = useState>({ data: null, - loading: true, // start as true + loading: true, error: null, }); diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index cddaa64768..80da86490e 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -150,6 +150,8 @@ export default [ ['ui_not_selected', 'Not selected'], ['ui_reset', 'Reset'], ['ui_documentaion', 'Documentation'], + ['ui_deny', 'Deny'], + ['ui_allow', 'Allow'], ['root_permission_denied', "You don't have permissions"], ['root_permission_no_permission', "You don't have permission for this action"], diff --git a/webapp/packages/core-localization/src/locales/fr.ts b/webapp/packages/core-localization/src/locales/fr.ts index 444e8bc508..cb6693488a 100644 --- a/webapp/packages/core-localization/src/locales/fr.ts +++ b/webapp/packages/core-localization/src/locales/fr.ts @@ -144,6 +144,8 @@ export default [ ['ui_not_selected', 'Not selected'], ['ui_reset', 'Reset'], ['ui_documentaion', 'Documentation'], + ['ui_deny', 'Refuser'], + ['ui_allow', 'Autoriser'], ['root_permission_denied', "Vous n'avez pas les permissions"], ['root_permission_no_permission', "Vous n'avez pas la permission pour cette action"], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index 3c45fd3ae2..f21778d697 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -141,6 +141,8 @@ export default [ ['ui_not_selected', 'Not selected'], ['ui_reset', 'Reset'], ['ui_documentaion', 'Documentation'], + ['ui_deny', 'Nega'], + ['ui_allow', 'Consenti'], ['root_permission_denied', 'Non hai i permessi'], ['app_root_session_expire_warning_title', 'La sessione sta per scadere'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 643cc29c06..e38154ad22 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -147,6 +147,8 @@ export default [ ['ui_not_selected', 'Не выбрано'], ['ui_reset', 'Сбросить'], ['ui_documentaion', 'Документация'], + ['ui_deny', 'Отклонить'], + ['ui_allow', 'Разрешить'], ['root_permission_denied', 'Отказано в доступе'], ['root_permission_no_permission', 'У вас нет разрешения на это действие'], diff --git a/webapp/packages/core-localization/src/locales/vi.ts b/webapp/packages/core-localization/src/locales/vi.ts index a41f739802..27d5c5895d 100644 --- a/webapp/packages/core-localization/src/locales/vi.ts +++ b/webapp/packages/core-localization/src/locales/vi.ts @@ -150,6 +150,8 @@ export default [ ['ui_not_selected', 'Not selected'], ['ui_reset', 'Reset'], ['ui_documentaion', 'Tài liệu'], + ['ui_deny', 'Từ chối'], + ['ui_allow', 'Cho phép'], ['root_permission_denied', 'Bạn không có quyền'], ['root_permission_no_permission', 'Bạn không có quyền thực hiện hành động này'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index 0cf4c32c53..b56a40c8c4 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -147,6 +147,8 @@ export default [ ['ui_not_selected', 'Not selected'], ['ui_reset', 'Reset'], ['ui_documentaion', '文档'], + ['ui_deny', '拒绝'], + ['ui_allow', '允许'], ['root_permission_denied', '您没有权限'], ['root_permission_no_permission', '您没有权限执行此操作'], From f2c2efeeee3350ff7369e151e81d89f51188ae49 Mon Sep 17 00:00:00 2001 From: naumov Date: Fri, 27 Mar 2026 12:41:05 +0100 Subject: [PATCH 3/6] dbeaver/pro#8746 set loading to false in case of abort --- webapp/packages/core-blocks/src/useQuery.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/packages/core-blocks/src/useQuery.ts b/webapp/packages/core-blocks/src/useQuery.ts index e1e4090511..2ee567701c 100644 --- a/webapp/packages/core-blocks/src/useQuery.ts +++ b/webapp/packages/core-blocks/src/useQuery.ts @@ -38,6 +38,7 @@ export function useQuery(queryFn: () => Promise, deps: any[] = []): QueryS .then(data => setState({ data, loading: false, error: null })) .catch(error => { if (error.name === 'AbortError') { + setState(prev => ({ ...prev, loading: false })); return; } From 7fbc099f3945adefdfc1fcb5b66729ad1079e373 Mon Sep 17 00:00:00 2001 From: naumov Date: Mon, 30 Mar 2026 15:30:34 +0200 Subject: [PATCH 4/6] dbeaver/pro#8746 use more advanced useQuery --- .../packages/core-blocks/src/useDidUpdate.ts | 39 +++ webapp/packages/core-blocks/src/useQuery.ts | 233 +++++++++++++++--- 2 files changed, 240 insertions(+), 32 deletions(-) create mode 100644 webapp/packages/core-blocks/src/useDidUpdate.ts diff --git a/webapp/packages/core-blocks/src/useDidUpdate.ts b/webapp/packages/core-blocks/src/useDidUpdate.ts new file mode 100644 index 0000000000..75be07ac88 --- /dev/null +++ b/webapp/packages/core-blocks/src/useDidUpdate.ts @@ -0,0 +1,39 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { type DependencyList, type EffectCallback, useLayoutEffect, useRef } from 'react'; + +/** + * @name useDidUpdate + * @description – Hook that triggers the effect callback on updates + + * @param {EffectCallback} effect The effect callback + * @param {DependencyList} [deps] The dependencies list for the effect + * + * @example + * useDidUpdate(() => console.log("effect runs on updates"), deps); + */ +export const useDidUpdate = (effect: EffectCallback, deps?: DependencyList) => { + const mountedRef = useRef(false); + + useLayoutEffect( + () => () => { + mountedRef.current = false; + }, + [], + ); + + useLayoutEffect(() => { + if (mountedRef.current) { + return effect(); + } + + mountedRef.current = true; + return undefined; + }, deps); +}; diff --git a/webapp/packages/core-blocks/src/useQuery.ts b/webapp/packages/core-blocks/src/useQuery.ts index 2ee567701c..0f3c4ce2c2 100644 --- a/webapp/packages/core-blocks/src/useQuery.ts +++ b/webapp/packages/core-blocks/src/useQuery.ts @@ -6,47 +6,216 @@ * you may not use this file except in compliance with the License. */ -import { useState, useEffect, useRef, useLayoutEffect } from 'react'; +import { type DependencyList, useEffect, useRef, useState } from 'react'; -interface QueryState { - data: T | null; - loading: boolean; - error: Error | null; +import { useDidUpdate } from './useDidUpdate.js'; + +/* The use query return type */ +export interface UseQueryOptions { + /* The enabled state of the query */ + enabled?: boolean; + /* The depends for the hook */ + keys?: DependencyList; + /* The placeholder data for the hook */ + placeholderData?: (() => Data) | Data; + /* The refetch interval */ + refetchInterval?: number; + /* The retry count of requests */ + retry?: boolean | number; + /* The retry delay of requests */ + retryDelay?: ((retry: number, error: Error) => number) | number; + /* The callback function to be invoked on error */ + onError?: (error: Error) => void; + /* The callback function to be invoked on success */ + onSuccess?: (data: Data) => void; + /* The select function to be invoked */ + select?: (data: QueryData) => Data; } -/** We can use this if we don’t have ILoadableState and we fetch data that is not supposed to be kept in the resource */ -export function useQuery(queryFn: () => Promise, deps: any[] = []): QueryState { - const [state, setState] = useState>({ - data: null, - loading: true, - error: null, - }); +interface UseQueryCallbackParams { + /* The depends for the hook */ + keys: DependencyList; + /* The abort signal */ + signal: AbortSignal; +} - const abortRef = useRef(null); - const queryFnRef = useRef(queryFn); +/* The use query return type */ +export interface UseQueryReturn { + /* The abort function */ + abort: AbortController['abort']; + /* The state of the query */ + data?: Data; + /* The success state of the query */ + error?: Error; + /* The error state of the query */ + isError: boolean; + /* The fetching state of the query */ + isFetching: boolean; + /* The loading state of the query */ + isLoading: boolean; + /* The refetching state of the query */ + isRefetching: boolean; + /* The success state of the query */ + isSuccess: boolean; + /* The refetch function */ + refetch: () => void; +} - useLayoutEffect(() => { - queryFnRef.current = queryFn; - }, [queryFn]); +/** + * @name useQuery + * @description - Hook that defines the logic when query data + * + * @template Data The type of the data + * @param {() => Promise} callback The callback function to be invoked + * @param {DependencyList} [options.keys] The dependencies for the hook + * @param {(data: Data) => void} [options.onSuccess] The callback function to be invoked on success + * @param {(error: Error) => void} [options.onError] The callback function to be invoked on error + * @param {UseQueryOptionsSelect} [options.select] The select function to be invoked + * @param {Data | (() => Data)} [options.initialData] The initial data for the hook + * @param {Data | (() => Data)} [options.placeholderData] The placeholder data for the hook + * @param {number} [options.refetchInterval] The refetch interval + * @param {boolean | number} [options.retry] The retry count of requests + * @returns {UseQueryReturn} An object with the state of the query + * + * @example + * const { data, isFetching, isLoading, isError, isSuccess, error, refetch, isRefetching, abort, aborted } = useQuery(() => fetch('url')); + */ +export const useQuery = ( + callback: (params: UseQueryCallbackParams) => Promise, + options?: UseQueryOptions, +): UseQueryReturn => { + const enabled = options?.enabled ?? true; + const retryCountRef = useRef(options?.retry ? getRetry(options.retry) : 0); + const alreadyRequested = useRef(false); - useEffect(() => { - abortRef.current?.abort(); - abortRef.current = new AbortController(); - - queryFnRef - .current() - .then(data => setState({ data, loading: false, error: null })) - .catch(error => { - if (error.name === 'AbortError') { - setState(prev => ({ ...prev, loading: false })); - return; + const [isFetching, setIsFetching] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + const [isRefetching, setIsRefetching] = useState(false); + const [isSuccess, setIsSuccess] = useState(!!options?.placeholderData); + + const [error, setError] = useState(undefined); + const [data, setData] = useState(options?.placeholderData); + + const abortControllerRef = useRef(new AbortController()); + const intervalIdRef = useRef>(undefined); + + const keys = options?.keys ?? []; + + const abort = () => { + abortControllerRef.current.abort(); + abortControllerRef.current = new AbortController(); + }; + + const request = (action: 'init' | 'refetch') => { + abort(); + + setIsFetching(true); + + if (action === 'init') { + alreadyRequested.current = true; + setIsLoading(true); + } + + if (action === 'refetch') { + setIsRefetching(true); + } + + callback({ signal: abortControllerRef.current.signal, keys }) + .then(response => { + const data = options?.select ? options?.select(response) : response; + options?.onSuccess?.(data as Data); + setData(data as Data); + setIsSuccess(true); + setError(undefined); + setIsError(false); + setIsFetching(false); + + if (action === 'init') { + setIsLoading(false); } - setState({ data: null, loading: false, error }); + if (action === 'refetch') { + setIsRefetching(false); + } + }) + .catch((error: Error) => { + if (retryCountRef.current > 0) { + retryCountRef.current -= 1; + const retryDelay = typeof options?.retryDelay === 'function' ? options?.retryDelay(retryCountRef.current, error) : options?.retryDelay; + + if (retryDelay) { + setTimeout(() => request(action), retryDelay); + return; + } + + return request(action); + } + options?.onError?.(error); + setData(undefined); + setIsSuccess(false); + setError(error); + setIsError(true); + setIsFetching(false); + + if (action === 'init') { + setIsLoading(false); + } + + if (action === 'refetch') { + setIsRefetching(false); + } + + retryCountRef.current = options?.retry ? getRetry(options.retry) : 0; + }) + .finally(() => { + if (options?.refetchInterval) { + const interval = setInterval(() => { + clearInterval(interval); + request('refetch'); + }, options?.refetchInterval); + intervalIdRef.current = interval; + } }); + }; + + useEffect(() => { + if (!enabled) { + return; + } + + request('init'); + }, []); + + useDidUpdate(() => { + if (!enabled) { + return; + } + + request(alreadyRequested.current ? 'refetch' : 'init'); + }, [enabled, ...keys]); + + useEffect(() => () => clearInterval(intervalIdRef.current), [enabled, options?.refetchInterval, options?.retry, ...keys]); + + const refetch = () => request('refetch'); + + return { + abort, + data, + error, + refetch, + isFetching, + isLoading, + isError, + isSuccess, + isRefetching, + }; +}; - return () => abortRef.current?.abort(); - }, deps); +function getRetry(retry: boolean | number) { + if (typeof retry === 'number') { + return retry; + } - return state; + return retry ? 1 : 0; } From 8c17856f0ee6b5e1c32e9b12f5f9d0f7482723e0 Mon Sep 17 00:00:00 2001 From: naumov Date: Mon, 30 Mar 2026 16:14:54 +0200 Subject: [PATCH 5/6] dbeaver/pro#8746 fix code style --- webapp/packages/core-blocks/src/useQuery.ts | 28 +++++++++++---------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/webapp/packages/core-blocks/src/useQuery.ts b/webapp/packages/core-blocks/src/useQuery.ts index 0f3c4ce2c2..364cbb9072 100644 --- a/webapp/packages/core-blocks/src/useQuery.ts +++ b/webapp/packages/core-blocks/src/useQuery.ts @@ -11,7 +11,7 @@ import { type DependencyList, useEffect, useRef, useState } from 'react'; import { useDidUpdate } from './useDidUpdate.js'; /* The use query return type */ -export interface UseQueryOptions { +export interface IUseQueryOptions { /* The enabled state of the query */ enabled?: boolean; /* The depends for the hook */ @@ -32,7 +32,7 @@ export interface UseQueryOptions { select?: (data: QueryData) => Data; } -interface UseQueryCallbackParams { +interface IUseQueryCallbackParams { /* The depends for the hook */ keys: DependencyList; /* The abort signal */ @@ -40,7 +40,7 @@ interface UseQueryCallbackParams { } /* The use query return type */ -export interface UseQueryReturn { +export interface IUseQueryReturn { /* The abort function */ abort: AbortController['abort']; /* The state of the query */ @@ -80,10 +80,10 @@ export interface UseQueryReturn { * @example * const { data, isFetching, isLoading, isError, isSuccess, error, refetch, isRefetching, abort, aborted } = useQuery(() => fetch('url')); */ -export const useQuery = ( - callback: (params: UseQueryCallbackParams) => Promise, - options?: UseQueryOptions, -): UseQueryReturn => { +export function useQuery( + callback: (params: IUseQueryCallbackParams) => Promise, + options?: IUseQueryOptions, +): IUseQueryReturn { const enabled = options?.enabled ?? true; const retryCountRef = useRef(options?.retry ? getRetry(options.retry) : 0); const alreadyRequested = useRef(false); @@ -102,12 +102,12 @@ export const useQuery = ( const keys = options?.keys ?? []; - const abort = () => { + function abort() { abortControllerRef.current.abort(); abortControllerRef.current = new AbortController(); - }; + } - const request = (action: 'init' | 'refetch') => { + function request(action: 'init' | 'refetch') { abort(); setIsFetching(true); @@ -177,7 +177,7 @@ export const useQuery = ( intervalIdRef.current = interval; } }); - }; + } useEffect(() => { if (!enabled) { @@ -197,7 +197,9 @@ export const useQuery = ( useEffect(() => () => clearInterval(intervalIdRef.current), [enabled, options?.refetchInterval, options?.retry, ...keys]); - const refetch = () => request('refetch'); + function refetch() { + request('refetch'); + } return { abort, @@ -210,7 +212,7 @@ export const useQuery = ( isSuccess, isRefetching, }; -}; +} function getRetry(retry: boolean | number) { if (typeof retry === 'number') { From 765c7fc925228583a6cf0b33bce7b9ebacf88d7d Mon Sep 17 00:00:00 2001 From: naumov Date: Mon, 30 Mar 2026 17:18:06 +0200 Subject: [PATCH 6/6] dbeaver/pro#8746 remove useQuery hook --- webapp/packages/core-blocks/src/index.ts | 1 - .../packages/core-blocks/src/useDidUpdate.ts | 39 --- webapp/packages/core-blocks/src/useQuery.ts | 223 ------------------ 3 files changed, 263 deletions(-) delete mode 100644 webapp/packages/core-blocks/src/useDidUpdate.ts delete mode 100644 webapp/packages/core-blocks/src/useQuery.ts diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index df42d64f27..7fcad9c718 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -253,4 +253,3 @@ export * from './ObjectPropertyInfo/evaluate.js'; export * from './ObjectPropertyInfo/getObjectPropertyDefaults.js'; export * from './useVisible.js'; export * from './SAVED_VALUE_INDICATOR.js'; -export * from './useQuery.js'; diff --git a/webapp/packages/core-blocks/src/useDidUpdate.ts b/webapp/packages/core-blocks/src/useDidUpdate.ts deleted file mode 100644 index 75be07ac88..0000000000 --- a/webapp/packages/core-blocks/src/useDidUpdate.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2026 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ - -import { type DependencyList, type EffectCallback, useLayoutEffect, useRef } from 'react'; - -/** - * @name useDidUpdate - * @description – Hook that triggers the effect callback on updates - - * @param {EffectCallback} effect The effect callback - * @param {DependencyList} [deps] The dependencies list for the effect - * - * @example - * useDidUpdate(() => console.log("effect runs on updates"), deps); - */ -export const useDidUpdate = (effect: EffectCallback, deps?: DependencyList) => { - const mountedRef = useRef(false); - - useLayoutEffect( - () => () => { - mountedRef.current = false; - }, - [], - ); - - useLayoutEffect(() => { - if (mountedRef.current) { - return effect(); - } - - mountedRef.current = true; - return undefined; - }, deps); -}; diff --git a/webapp/packages/core-blocks/src/useQuery.ts b/webapp/packages/core-blocks/src/useQuery.ts deleted file mode 100644 index 364cbb9072..0000000000 --- a/webapp/packages/core-blocks/src/useQuery.ts +++ /dev/null @@ -1,223 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2026 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ - -import { type DependencyList, useEffect, useRef, useState } from 'react'; - -import { useDidUpdate } from './useDidUpdate.js'; - -/* The use query return type */ -export interface IUseQueryOptions { - /* The enabled state of the query */ - enabled?: boolean; - /* The depends for the hook */ - keys?: DependencyList; - /* The placeholder data for the hook */ - placeholderData?: (() => Data) | Data; - /* The refetch interval */ - refetchInterval?: number; - /* The retry count of requests */ - retry?: boolean | number; - /* The retry delay of requests */ - retryDelay?: ((retry: number, error: Error) => number) | number; - /* The callback function to be invoked on error */ - onError?: (error: Error) => void; - /* The callback function to be invoked on success */ - onSuccess?: (data: Data) => void; - /* The select function to be invoked */ - select?: (data: QueryData) => Data; -} - -interface IUseQueryCallbackParams { - /* The depends for the hook */ - keys: DependencyList; - /* The abort signal */ - signal: AbortSignal; -} - -/* The use query return type */ -export interface IUseQueryReturn { - /* The abort function */ - abort: AbortController['abort']; - /* The state of the query */ - data?: Data; - /* The success state of the query */ - error?: Error; - /* The error state of the query */ - isError: boolean; - /* The fetching state of the query */ - isFetching: boolean; - /* The loading state of the query */ - isLoading: boolean; - /* The refetching state of the query */ - isRefetching: boolean; - /* The success state of the query */ - isSuccess: boolean; - /* The refetch function */ - refetch: () => void; -} - -/** - * @name useQuery - * @description - Hook that defines the logic when query data - * - * @template Data The type of the data - * @param {() => Promise} callback The callback function to be invoked - * @param {DependencyList} [options.keys] The dependencies for the hook - * @param {(data: Data) => void} [options.onSuccess] The callback function to be invoked on success - * @param {(error: Error) => void} [options.onError] The callback function to be invoked on error - * @param {UseQueryOptionsSelect} [options.select] The select function to be invoked - * @param {Data | (() => Data)} [options.initialData] The initial data for the hook - * @param {Data | (() => Data)} [options.placeholderData] The placeholder data for the hook - * @param {number} [options.refetchInterval] The refetch interval - * @param {boolean | number} [options.retry] The retry count of requests - * @returns {UseQueryReturn} An object with the state of the query - * - * @example - * const { data, isFetching, isLoading, isError, isSuccess, error, refetch, isRefetching, abort, aborted } = useQuery(() => fetch('url')); - */ -export function useQuery( - callback: (params: IUseQueryCallbackParams) => Promise, - options?: IUseQueryOptions, -): IUseQueryReturn { - const enabled = options?.enabled ?? true; - const retryCountRef = useRef(options?.retry ? getRetry(options.retry) : 0); - const alreadyRequested = useRef(false); - - const [isFetching, setIsFetching] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isError, setIsError] = useState(false); - const [isRefetching, setIsRefetching] = useState(false); - const [isSuccess, setIsSuccess] = useState(!!options?.placeholderData); - - const [error, setError] = useState(undefined); - const [data, setData] = useState(options?.placeholderData); - - const abortControllerRef = useRef(new AbortController()); - const intervalIdRef = useRef>(undefined); - - const keys = options?.keys ?? []; - - function abort() { - abortControllerRef.current.abort(); - abortControllerRef.current = new AbortController(); - } - - function request(action: 'init' | 'refetch') { - abort(); - - setIsFetching(true); - - if (action === 'init') { - alreadyRequested.current = true; - setIsLoading(true); - } - - if (action === 'refetch') { - setIsRefetching(true); - } - - callback({ signal: abortControllerRef.current.signal, keys }) - .then(response => { - const data = options?.select ? options?.select(response) : response; - options?.onSuccess?.(data as Data); - setData(data as Data); - setIsSuccess(true); - setError(undefined); - setIsError(false); - setIsFetching(false); - - if (action === 'init') { - setIsLoading(false); - } - - if (action === 'refetch') { - setIsRefetching(false); - } - }) - .catch((error: Error) => { - if (retryCountRef.current > 0) { - retryCountRef.current -= 1; - const retryDelay = typeof options?.retryDelay === 'function' ? options?.retryDelay(retryCountRef.current, error) : options?.retryDelay; - - if (retryDelay) { - setTimeout(() => request(action), retryDelay); - return; - } - - return request(action); - } - options?.onError?.(error); - setData(undefined); - setIsSuccess(false); - setError(error); - setIsError(true); - setIsFetching(false); - - if (action === 'init') { - setIsLoading(false); - } - - if (action === 'refetch') { - setIsRefetching(false); - } - - retryCountRef.current = options?.retry ? getRetry(options.retry) : 0; - }) - .finally(() => { - if (options?.refetchInterval) { - const interval = setInterval(() => { - clearInterval(interval); - request('refetch'); - }, options?.refetchInterval); - intervalIdRef.current = interval; - } - }); - } - - useEffect(() => { - if (!enabled) { - return; - } - - request('init'); - }, []); - - useDidUpdate(() => { - if (!enabled) { - return; - } - - request(alreadyRequested.current ? 'refetch' : 'init'); - }, [enabled, ...keys]); - - useEffect(() => () => clearInterval(intervalIdRef.current), [enabled, options?.refetchInterval, options?.retry, ...keys]); - - function refetch() { - request('refetch'); - } - - return { - abort, - data, - error, - refetch, - isFetching, - isLoading, - isError, - isSuccess, - isRefetching, - }; -} - -function getRetry(retry: boolean | number) { - if (typeof retry === 'number') { - return retry; - } - - return retry ? 1 : 0; -}