fix(hooks): memoize return objects to prevent infinite render loops

Apply useMemo to hook returns that create new object references on each
render. This prevents downstream useEffect/useCallback dependencies from
detecting false changes and triggering re-execution.

Fixed hooks:
- PrivacyProvider: memoize context value and toggle callback
- useCopilot: wrap large return object with all dependencies
- useWebSocket: memoize status/connect/disconnect object
- useProjectSelection: memoize state and handlers object
- useCliproxyAuthFlow: memoize spread state with auth functions
- useAnalyticsPage: memoize apiOptions to stabilize query params
This commit is contained in:
kaitranntt committed 2025-12-21 04:17:32 -05:00
1 parent b9e69965e2
commit f15b989508
6 files changed
+73 -19

No files matched your search

+15 -6
View File
@@ -5,7 +5,15 @@
*/ */
/* eslint-disable react-refresh/only-export-components */ /* 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 { interface PrivacyContextValue {
/** Whether privacy/demo mode is enabled */ /** Whether privacy/demo mode is enabled */
@@ -32,13 +40,14 @@ export function PrivacyProvider({ children }: { children: ReactNode }) {
localStorage.setItem(STORAGE_KEY, String(privacyMode)); localStorage.setItem(STORAGE_KEY, String(privacyMode));
}, [privacyMode]); }, [privacyMode]);
const togglePrivacyMode = () => setPrivacyMode((prev) => !prev); const togglePrivacyMode = useCallback(() => setPrivacyMode((prev) => !prev), []);
return ( const value = useMemo(
<PrivacyContext.Provider value={{ privacyMode, togglePrivacyMode }}> () => ({ privacyMode, togglePrivacyMode }),
{children} [privacyMode, togglePrivacyMode]
</PrivacyContext.Provider>
); );
return <PrivacyContext.Provider value={value}>{children}</PrivacyContext.Provider>;
} }
export function usePrivacy() { export function usePrivacy() {
+6 -3
View File
@@ -3,7 +3,7 @@
* Manages popup-based OAuth authentication flows * 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 { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -180,9 +180,12 @@ export function useCliproxyAuthFlow() {
setState({ provider: null, isAuthenticating: false, error: null }); setState({ provider: null, isAuthenticating: false, error: null });
}, [cleanup]); }, [cleanup]);
return { return useMemo(
() => ({
...state, ...state,
startAuth, startAuth,
cancelAuth, cancelAuth,
}; }),
[state, startAuth, cancelAuth]
);
} }
+38 -2
View File
@@ -4,6 +4,7 @@
* React hook for managing GitHub Copilot integration state. * React hook for managing GitHub Copilot integration state.
*/ */
import { useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
const API_BASE = '/api'; const API_BASE = '/api';
@@ -247,7 +248,8 @@ export function useCopilot() {
}, },
}); });
return { return useMemo(
() => ({
// Status // Status
status: statusQuery.data, status: statusQuery.data,
statusLoading: statusQuery.isLoading, statusLoading: statusQuery.isLoading,
@@ -296,5 +298,39 @@ export function useCopilot() {
install: installMutation.mutate, install: installMutation.mutate,
installAsync: installMutation.mutateAsync, installAsync: installMutation.mutateAsync,
isInstalling: installMutation.isPending, 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,
]
);
} }
+6 -3
View File
@@ -5,7 +5,7 @@
* Used in conjunction with ProjectSelectionDialog component. * Used in conjunction with ProjectSelectionDialog component.
*/ */
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useMemo } from 'react';
interface GCloudProject { interface GCloudProject {
id: string; id: string;
@@ -102,10 +102,13 @@ export function useProjectSelection() {
setState({ isOpen: false, prompt: null }); setState({ isOpen: false, prompt: null });
}, []); }, []);
return { return useMemo(
() => ({
isOpen: state.isOpen, isOpen: state.isOpen,
prompt: state.prompt, prompt: state.prompt,
onSelect: handleSelect, onSelect: handleSelect,
onClose: handleClose, onClose: handleClose,
}; }),
[state.isOpen, state.prompt, handleSelect, handleClose]
);
} }
+2 -2
View File
@@ -4,7 +4,7 @@
* Manages WebSocket connection, auto-reconnect, and React Query invalidation. * 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 { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -141,5 +141,5 @@ export function useWebSocket() {
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
return { status, connect, disconnect }; return useMemo(() => ({ status, connect, disconnect }), [status, connect, disconnect]);
} }
+6 -3
View File
@@ -41,11 +41,14 @@ export function useAnalyticsPage() {
} }
}, [refreshUsage]); }, [refreshUsage]);
// Convert dates to API format // Convert dates to API format - memoized to prevent unnecessary re-renders
const apiOptions = { const apiOptions = useMemo(
() => ({
startDate: dateRange?.from, startDate: dateRange?.from,
endDate: dateRange?.to, endDate: dateRange?.to,
}; }),
[dateRange?.from, dateRange?.to]
);
// Fetch data // Fetch data
const { data: summary, isLoading: isSummaryLoading } = useUsageSummary(apiOptions); const { data: summary, isLoading: isSummaryLoading } = useUsageSummary(apiOptions);