mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 22:14:22 +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:
1 parent
b9e69965e2
commit
f15b989508
6 files changed
+120
-66
No files matched your search
@@ -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() {
|
||||||
|
|||||||
@@ -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,
|
() => ({
|
||||||
startAuth,
|
...state,
|
||||||
cancelAuth,
|
startAuth,
|
||||||
};
|
cancelAuth,
|
||||||
|
}),
|
||||||
|
[state, startAuth, cancelAuth]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
+76
-40
@@ -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,54 +248,89 @@ export function useCopilot() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return useMemo(
|
||||||
// Status
|
() => ({
|
||||||
status: statusQuery.data,
|
// Status
|
||||||
statusLoading: statusQuery.isLoading,
|
status: statusQuery.data,
|
||||||
statusError: statusQuery.error,
|
statusLoading: statusQuery.isLoading,
|
||||||
refetchStatus: statusQuery.refetch,
|
statusError: statusQuery.error,
|
||||||
|
refetchStatus: statusQuery.refetch,
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
config: configQuery.data,
|
config: configQuery.data,
|
||||||
configLoading: configQuery.isLoading,
|
configLoading: configQuery.isLoading,
|
||||||
|
|
||||||
// Models
|
// Models
|
||||||
models: modelsQuery.data?.models ?? [],
|
models: modelsQuery.data?.models ?? [],
|
||||||
currentModel: modelsQuery.data?.current,
|
currentModel: modelsQuery.data?.current,
|
||||||
modelsLoading: modelsQuery.isLoading,
|
modelsLoading: modelsQuery.isLoading,
|
||||||
|
|
||||||
// Raw Settings
|
// Raw Settings
|
||||||
rawSettings: rawSettingsQuery.data,
|
rawSettings: rawSettingsQuery.data,
|
||||||
rawSettingsLoading: rawSettingsQuery.isLoading,
|
rawSettingsLoading: rawSettingsQuery.isLoading,
|
||||||
refetchRawSettings: rawSettingsQuery.refetch,
|
refetchRawSettings: rawSettingsQuery.refetch,
|
||||||
|
|
||||||
// Mutations
|
// Mutations
|
||||||
updateConfig: updateConfigMutation.mutate,
|
updateConfig: updateConfigMutation.mutate,
|
||||||
updateConfigAsync: updateConfigMutation.mutateAsync,
|
updateConfigAsync: updateConfigMutation.mutateAsync,
|
||||||
isUpdating: updateConfigMutation.isPending,
|
isUpdating: updateConfigMutation.isPending,
|
||||||
|
|
||||||
saveRawSettings: saveRawSettingsMutation.mutate,
|
saveRawSettings: saveRawSettingsMutation.mutate,
|
||||||
saveRawSettingsAsync: saveRawSettingsMutation.mutateAsync,
|
saveRawSettingsAsync: saveRawSettingsMutation.mutateAsync,
|
||||||
isSavingRawSettings: saveRawSettingsMutation.isPending,
|
isSavingRawSettings: saveRawSettingsMutation.isPending,
|
||||||
|
|
||||||
startAuth: startAuthMutation.mutate,
|
startAuth: startAuthMutation.mutate,
|
||||||
startAuthAsync: startAuthMutation.mutateAsync,
|
startAuthAsync: startAuthMutation.mutateAsync,
|
||||||
isAuthenticating: startAuthMutation.isPending,
|
isAuthenticating: startAuthMutation.isPending,
|
||||||
authResult: startAuthMutation.data,
|
authResult: startAuthMutation.data,
|
||||||
|
|
||||||
startDaemon: startDaemonMutation.mutate,
|
startDaemon: startDaemonMutation.mutate,
|
||||||
isStartingDaemon: startDaemonMutation.isPending,
|
isStartingDaemon: startDaemonMutation.isPending,
|
||||||
|
|
||||||
stopDaemon: stopDaemonMutation.mutate,
|
stopDaemon: stopDaemonMutation.mutate,
|
||||||
isStoppingDaemon: stopDaemonMutation.isPending,
|
isStoppingDaemon: stopDaemonMutation.isPending,
|
||||||
|
|
||||||
// Install
|
// Install
|
||||||
info: infoQuery.data,
|
info: infoQuery.data,
|
||||||
infoLoading: infoQuery.isLoading,
|
infoLoading: infoQuery.isLoading,
|
||||||
refetchInfo: infoQuery.refetch,
|
refetchInfo: infoQuery.refetch,
|
||||||
|
|
||||||
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,
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -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,
|
() => ({
|
||||||
prompt: state.prompt,
|
isOpen: state.isOpen,
|
||||||
onSelect: handleSelect,
|
prompt: state.prompt,
|
||||||
onClose: handleClose,
|
onSelect: handleSelect,
|
||||||
};
|
onClose: handleClose,
|
||||||
|
}),
|
||||||
|
[state.isOpen, state.prompt, handleSelect, handleClose]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
@@ -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,
|
() => ({
|
||||||
endDate: dateRange?.to,
|
startDate: dateRange?.from,
|
||||||
};
|
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);
|
||||||
|
|||||||
Reference in new issue
Block a user