diff options
| author | Oliver <oliver@mohlin.dev> | 2025-10-09 13:57:40 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-10-10 13:38:06 +0200 |
| commit | d51d35bc3883556309a8211410ec2cb0f69cd4e1 (patch) | |
| tree | 24bf2d9c62400a27c1632494f6c71ff9e9b3ded6 | |
| parent | 2903681572d2b0e816e4b7706b92af47b79df570 (diff) | |
| download | mullvadvpn-d51d35bc3883556309a8211410ec2cb0f69cd4e1.tar.xz mullvadvpn-d51d35bc3883556309a8211410ec2cb0f69cd4e1.zip | |
Add useQuery hook
| -rw-r--r-- | desktop/packages/mullvad-vpn/src/renderer/lib/hooks/index.ts | 1 | ||||
| -rw-r--r-- | desktop/packages/mullvad-vpn/src/renderer/lib/hooks/use-query.ts | 58 |
2 files changed, 59 insertions, 0 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/hooks/index.ts index e75fe95d06..c262425b53 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/lib/hooks/index.ts +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/hooks/index.ts @@ -1,2 +1,3 @@ export * from './use-exclusive-task'; export * from './use-roving-focus'; +export * from './use-query'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/hooks/use-query.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/hooks/use-query.ts new file mode 100644 index 0000000000..7bd63e65b8 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/hooks/use-query.ts @@ -0,0 +1,58 @@ +import React from 'react'; + +export type UseQueryProps<T> = { + enabled?: boolean; + queryFn: () => Promise<T>; + deps: React.DependencyList; +}; + +export const useQuery = <T>({ queryFn, enabled = true, deps }: UseQueryProps<T>) => { + const [data, setData] = React.useState<T | undefined>(undefined); + const [error, setError] = React.useState<Error | undefined>(undefined); + const [isError, setIsError] = React.useState<boolean>(false); + const [isFetching, setIsFetching] = React.useState<boolean>(false); + const [hasSettledFirst, setHasSettledFirst] = React.useState<boolean>(false); + + const mountedRef = React.useRef(false); + const runIdRef = React.useRef(0); + + const run = React.useCallback(async () => { + const runId = ++runIdRef.current; + + const isActive = () => mountedRef.current && runId === runIdRef.current; + + setIsFetching(true); + setIsError(false); + setError(undefined); + + try { + const result = await queryFn(); + if (isActive()) { + setData(result); + } + } catch (err) { + if (isActive()) { + setIsError(true); + setError(err as Error); + } + } finally { + if (isActive()) setIsFetching(false); + if (!hasSettledFirst) setHasSettledFirst(true); + } + }, [hasSettledFirst, queryFn]); + + const isLoading = isFetching && !hasSettledFirst; + + React.useEffect(() => { + mountedRef.current = true; + if (enabled) void run(); + return () => { + mountedRef.current = false; + }; + // Excluding run from deps to avoid re-fetching on every render + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [enabled, ...deps]); + + return { data, error, isError, isLoading, isFetching, refetch: run }; +}; |
