diff --git a/ui/src/contexts/privacy-context.tsx b/ui/src/contexts/privacy-context.tsx
index 98cc98fb..3cf6cbcb 100644
--- a/ui/src/contexts/privacy-context.tsx
+++ b/ui/src/contexts/privacy-context.tsx
@@ -5,7 +5,15 @@
*/
/* eslint-disable react-refresh/only-export-components */
-import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
+import {
+ createContext,
+ useContext,
+ useState,
+ useEffect,
+ useMemo,
+ useCallback,
+ type ReactNode,
+} from 'react';
interface PrivacyContextValue {
/** Whether privacy/demo mode is enabled */
@@ -32,13 +40,14 @@ export function PrivacyProvider({ children }: { children: ReactNode }) {
localStorage.setItem(STORAGE_KEY, String(privacyMode));
}, [privacyMode]);
- const togglePrivacyMode = () => setPrivacyMode((prev) => !prev);
+ const togglePrivacyMode = useCallback(() => setPrivacyMode((prev) => !prev), []);
- return (
-
- {children}
-
+ const value = useMemo(
+ () => ({ privacyMode, togglePrivacyMode }),
+ [privacyMode, togglePrivacyMode]
);
+
+ return {children};
}
export function usePrivacy() {
diff --git a/ui/src/hooks/use-cliproxy-auth-flow.ts b/ui/src/hooks/use-cliproxy-auth-flow.ts
index 103d98e0..e896c3f3 100644
--- a/ui/src/hooks/use-cliproxy-auth-flow.ts
+++ b/ui/src/hooks/use-cliproxy-auth-flow.ts
@@ -3,7 +3,7 @@
* Manages popup-based OAuth authentication flows
*/
-import { useState, useCallback, useRef, useEffect } from 'react';
+import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -180,9 +180,12 @@ export function useCliproxyAuthFlow() {
setState({ provider: null, isAuthenticating: false, error: null });
}, [cleanup]);
- return {
- ...state,
- startAuth,
- cancelAuth,
- };
+ return useMemo(
+ () => ({
+ ...state,
+ startAuth,
+ cancelAuth,
+ }),
+ [state, startAuth, cancelAuth]
+ );
}
diff --git a/ui/src/hooks/use-copilot.ts b/ui/src/hooks/use-copilot.ts
index 3431ffdb..2aed765e 100644
--- a/ui/src/hooks/use-copilot.ts
+++ b/ui/src/hooks/use-copilot.ts
@@ -4,6 +4,7 @@
* React hook for managing GitHub Copilot integration state.
*/
+import { useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
const API_BASE = '/api';
@@ -247,54 +248,89 @@ export function useCopilot() {
},
});
- return {
- // Status
- status: statusQuery.data,
- statusLoading: statusQuery.isLoading,
- statusError: statusQuery.error,
- refetchStatus: statusQuery.refetch,
+ return useMemo(
+ () => ({
+ // Status
+ status: statusQuery.data,
+ statusLoading: statusQuery.isLoading,
+ statusError: statusQuery.error,
+ refetchStatus: statusQuery.refetch,
- // Config
- config: configQuery.data,
- configLoading: configQuery.isLoading,
+ // Config
+ config: configQuery.data,
+ configLoading: configQuery.isLoading,
- // Models
- models: modelsQuery.data?.models ?? [],
- currentModel: modelsQuery.data?.current,
- modelsLoading: modelsQuery.isLoading,
+ // Models
+ models: modelsQuery.data?.models ?? [],
+ currentModel: modelsQuery.data?.current,
+ modelsLoading: modelsQuery.isLoading,
- // Raw Settings
- rawSettings: rawSettingsQuery.data,
- rawSettingsLoading: rawSettingsQuery.isLoading,
- refetchRawSettings: rawSettingsQuery.refetch,
+ // Raw Settings
+ rawSettings: rawSettingsQuery.data,
+ rawSettingsLoading: rawSettingsQuery.isLoading,
+ refetchRawSettings: rawSettingsQuery.refetch,
- // Mutations
- updateConfig: updateConfigMutation.mutate,
- updateConfigAsync: updateConfigMutation.mutateAsync,
- isUpdating: updateConfigMutation.isPending,
+ // Mutations
+ updateConfig: updateConfigMutation.mutate,
+ updateConfigAsync: updateConfigMutation.mutateAsync,
+ isUpdating: updateConfigMutation.isPending,
- saveRawSettings: saveRawSettingsMutation.mutate,
- saveRawSettingsAsync: saveRawSettingsMutation.mutateAsync,
- isSavingRawSettings: saveRawSettingsMutation.isPending,
+ saveRawSettings: saveRawSettingsMutation.mutate,
+ saveRawSettingsAsync: saveRawSettingsMutation.mutateAsync,
+ isSavingRawSettings: saveRawSettingsMutation.isPending,
- startAuth: startAuthMutation.mutate,
- startAuthAsync: startAuthMutation.mutateAsync,
- isAuthenticating: startAuthMutation.isPending,
- authResult: startAuthMutation.data,
+ startAuth: startAuthMutation.mutate,
+ startAuthAsync: startAuthMutation.mutateAsync,
+ isAuthenticating: startAuthMutation.isPending,
+ authResult: startAuthMutation.data,
- startDaemon: startDaemonMutation.mutate,
- isStartingDaemon: startDaemonMutation.isPending,
+ startDaemon: startDaemonMutation.mutate,
+ isStartingDaemon: startDaemonMutation.isPending,
- stopDaemon: stopDaemonMutation.mutate,
- isStoppingDaemon: stopDaemonMutation.isPending,
+ stopDaemon: stopDaemonMutation.mutate,
+ isStoppingDaemon: stopDaemonMutation.isPending,
- // Install
- info: infoQuery.data,
- infoLoading: infoQuery.isLoading,
- refetchInfo: infoQuery.refetch,
+ // Install
+ info: infoQuery.data,
+ infoLoading: infoQuery.isLoading,
+ refetchInfo: infoQuery.refetch,
- install: installMutation.mutate,
- installAsync: installMutation.mutateAsync,
- isInstalling: installMutation.isPending,
- };
+ install: installMutation.mutate,
+ installAsync: installMutation.mutateAsync,
+ isInstalling: installMutation.isPending,
+ }),
+ [
+ statusQuery.data,
+ statusQuery.isLoading,
+ statusQuery.error,
+ statusQuery.refetch,
+ configQuery.data,
+ configQuery.isLoading,
+ modelsQuery.data,
+ modelsQuery.isLoading,
+ rawSettingsQuery.data,
+ rawSettingsQuery.isLoading,
+ rawSettingsQuery.refetch,
+ updateConfigMutation.mutate,
+ updateConfigMutation.mutateAsync,
+ updateConfigMutation.isPending,
+ saveRawSettingsMutation.mutate,
+ saveRawSettingsMutation.mutateAsync,
+ saveRawSettingsMutation.isPending,
+ startAuthMutation.mutate,
+ startAuthMutation.mutateAsync,
+ startAuthMutation.isPending,
+ startAuthMutation.data,
+ startDaemonMutation.mutate,
+ startDaemonMutation.isPending,
+ stopDaemonMutation.mutate,
+ stopDaemonMutation.isPending,
+ infoQuery.data,
+ infoQuery.isLoading,
+ infoQuery.refetch,
+ installMutation.mutate,
+ installMutation.mutateAsync,
+ installMutation.isPending,
+ ]
+ );
}
diff --git a/ui/src/hooks/use-project-selection.ts b/ui/src/hooks/use-project-selection.ts
index 89f16e68..8e466178 100644
--- a/ui/src/hooks/use-project-selection.ts
+++ b/ui/src/hooks/use-project-selection.ts
@@ -5,7 +5,7 @@
* Used in conjunction with ProjectSelectionDialog component.
*/
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect, useCallback, useMemo } from 'react';
interface GCloudProject {
id: string;
@@ -102,10 +102,13 @@ export function useProjectSelection() {
setState({ isOpen: false, prompt: null });
}, []);
- return {
- isOpen: state.isOpen,
- prompt: state.prompt,
- onSelect: handleSelect,
- onClose: handleClose,
- };
+ return useMemo(
+ () => ({
+ isOpen: state.isOpen,
+ prompt: state.prompt,
+ onSelect: handleSelect,
+ onClose: handleClose,
+ }),
+ [state.isOpen, state.prompt, handleSelect, handleClose]
+ );
}
diff --git a/ui/src/hooks/use-websocket.ts b/ui/src/hooks/use-websocket.ts
index 8ff5de46..16abc65f 100644
--- a/ui/src/hooks/use-websocket.ts
+++ b/ui/src/hooks/use-websocket.ts
@@ -4,7 +4,7 @@
* Manages WebSocket connection, auto-reconnect, and React Query invalidation.
*/
-import { useEffect, useState, useCallback, useRef } from 'react';
+import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -141,5 +141,5 @@ export function useWebSocket() {
return () => clearInterval(interval);
}, []);
- return { status, connect, disconnect };
+ return useMemo(() => ({ status, connect, disconnect }), [status, connect, disconnect]);
}
diff --git a/ui/src/pages/analytics/hooks.ts b/ui/src/pages/analytics/hooks.ts
index 8f8e39a7..f4fc73ef 100644
--- a/ui/src/pages/analytics/hooks.ts
+++ b/ui/src/pages/analytics/hooks.ts
@@ -41,11 +41,14 @@ export function useAnalyticsPage() {
}
}, [refreshUsage]);
- // Convert dates to API format
- const apiOptions = {
- startDate: dateRange?.from,
- endDate: dateRange?.to,
- };
+ // Convert dates to API format - memoized to prevent unnecessary re-renders
+ const apiOptions = useMemo(
+ () => ({
+ startDate: dateRange?.from,
+ endDate: dateRange?.to,
+ }),
+ [dateRange?.from, dateRange?.to]
+ );
// Fetch data
const { data: summary, isLoading: isSummaryLoading } = useUsageSummary(apiOptions);