summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOliver <oliver@mohlin.dev>2025-10-09 13:57:40 +0200
committerTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-10-10 13:38:06 +0200
commitd51d35bc3883556309a8211410ec2cb0f69cd4e1 (patch)
tree24bf2d9c62400a27c1632494f6c71ff9e9b3ded6
parent2903681572d2b0e816e4b7706b92af47b79df570 (diff)
downloadmullvadvpn-d51d35bc3883556309a8211410ec2cb0f69cd4e1.tar.xz
mullvadvpn-d51d35bc3883556309a8211410ec2cb0f69cd4e1.zip
Add useQuery hook
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/hooks/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/hooks/use-query.ts58
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 };
+};