mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 04:17:11 +00:00
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:
@@ -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 (
|
||||
<PrivacyContext.Provider value={{ privacyMode, togglePrivacyMode }}>
|
||||
{children}
|
||||
</PrivacyContext.Provider>
|
||||
const value = useMemo(
|
||||
() => ({ privacyMode, togglePrivacyMode }),
|
||||
[privacyMode, togglePrivacyMode]
|
||||
);
|
||||
|
||||
return <PrivacyContext.Provider value={value}>{children}</PrivacyContext.Provider>;
|
||||
}
|
||||
|
||||
export function usePrivacy() {
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
+76
-40
@@ -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,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user