refactor: use React in its intended way (#3963)
* refactor: replace `useEffect` w/ `useLocalStorage` * refactor: replace `useEffect` w/ `useSWR` * refactor: replace `useEffect` and `useSWR`. clean up `useRef` * refactor: use `requestIdleCallback` * refactor: replace `useEffect` w/ `useMemo` * fix: clean up `useEffect` * refactor: replace `useEffect` w/ `useSWR` * refactor: remove unused `useCallback` * refactor: enhance performance and memory management in frontend processes * refactor: improve pre-push script structure and readability --------- Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com> Co-authored-by: Tunglies <tunglies.dev@outlook.com>
This commit is contained in:
@@ -11,18 +11,24 @@ if git diff --cached --name-only | grep -q '^src-tauri/'; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# 只在 push 到 origin 并且 origin 指向目标仓库时执行格式检查
|
||||
if [ "$1" = "origin" ] && echo "$2" | grep -Eq 'github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$'; then
|
||||
echo "[pre-push] Detected push to origin (clash-verge-rev/clash-verge-rev)"
|
||||
echo "[pre-push] Running pnpm format:check..."
|
||||
|
||||
# Only run format check if the remote exists and is the main repo
|
||||
remote_name="$1"
|
||||
if git remote get-url "$remote_name" >/dev/null 2>&1; then
|
||||
remote_url=$(git remote get-url "$remote_name")
|
||||
if [[ "$remote_url" =~ github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$ ]]; then
|
||||
echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev ($remote_url)"
|
||||
echo "[pre-push] Running pnpm format:check..."
|
||||
pnpm format:check
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Code format check failed. Please fix formatting before pushing."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
else
|
||||
echo "[pre-push] Not pushing to target repo. Skipping format check."
|
||||
fi
|
||||
else
|
||||
echo "[pre-push] Remote $remote_name does not exist. Skipping format check."
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -53,7 +53,8 @@
|
||||
|
||||
- 优化 托盘 统一响应
|
||||
- 优化 静默启动+自启动轻量模式 运行方式
|
||||
- 升级依赖
|
||||
- 降低前端潜在内存泄漏风险,提升运行时性能
|
||||
- 优化 React 状态、副作用、数据获取、清理等流程。
|
||||
|
||||
## v2.3.0
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { DataGrid, GridColDef, GridColumnResizeParams } from "@mui/x-data-grid";
|
||||
import { useThemeMode } from "@/services/states";
|
||||
import { truncateStr } from "@/utils/truncate-str";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { t } from "i18next";
|
||||
import { useLocalStorage } from "foxact/use-local-storage";
|
||||
|
||||
interface Props {
|
||||
connections: IConnectionsItem[];
|
||||
@@ -21,11 +22,13 @@ export const ConnectionTable = (props: Props) => {
|
||||
Partial<Record<keyof IConnectionsItem, boolean>>
|
||||
>({});
|
||||
|
||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(
|
||||
() => {
|
||||
const saved = localStorage.getItem("connection-table-widths");
|
||||
return saved ? JSON.parse(saved) : {};
|
||||
},
|
||||
const [columnWidths, setColumnWidths] = useLocalStorage<
|
||||
Record<string, number>
|
||||
>(
|
||||
"connection-table-widths",
|
||||
// server-side value, this is the default value used by server-side rendering (if any)
|
||||
// Do not omit (otherwise a Suspense boundary will be triggered)
|
||||
{},
|
||||
);
|
||||
|
||||
const [columns] = useState<GridColDef[]>([
|
||||
@@ -116,14 +119,6 @@ export const ConnectionTable = (props: Props) => {
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Saving column widths:", columnWidths);
|
||||
localStorage.setItem(
|
||||
"connection-table-widths",
|
||||
JSON.stringify(columnWidths),
|
||||
);
|
||||
}, [columnWidths]);
|
||||
|
||||
const handleColumnResize = (params: GridColumnResizeParams) => {
|
||||
const { colDef, width } = params;
|
||||
console.log("Column resize:", colDef.field, width);
|
||||
|
||||
@@ -29,6 +29,7 @@ import parseTraffic from "@/utils/parse-traffic";
|
||||
import { isDebugEnabled, gc } from "@/services/api";
|
||||
import { ReactNode } from "react";
|
||||
import { useAppData } from "@/providers/app-data-provider";
|
||||
import useSWR from "swr";
|
||||
|
||||
interface MemoryUsage {
|
||||
inuse: number;
|
||||
@@ -161,7 +162,6 @@ export const EnhancedTrafficStats = () => {
|
||||
const { verge } = useVerge();
|
||||
const trafficRef = useRef<EnhancedTrafficGraphRef>(null);
|
||||
const pageVisible = useVisibility();
|
||||
const [isDebug, setIsDebug] = useState(false);
|
||||
|
||||
// 使用AppDataProvider
|
||||
const { connections, uptime } = useAppData();
|
||||
@@ -178,19 +178,16 @@ export const EnhancedTrafficStats = () => {
|
||||
// 是否显示流量图表
|
||||
const trafficGraph = verge?.traffic_graph ?? true;
|
||||
|
||||
// WebSocket引用
|
||||
const socketRefs = useRef<{
|
||||
traffic: ReturnType<typeof createAuthSockette> | null;
|
||||
memory: ReturnType<typeof createAuthSockette> | null;
|
||||
}>({
|
||||
traffic: null,
|
||||
memory: null,
|
||||
});
|
||||
|
||||
// 检查是否支持调试
|
||||
useEffect(() => {
|
||||
isDebugEnabled().then((flag) => setIsDebug(flag));
|
||||
}, []);
|
||||
// TODO: merge this hook with layout-traffic.tsx
|
||||
const { data: isDebug } = useSWR(
|
||||
`clash-verge-rev-internal://isDebugEnabled`,
|
||||
() => isDebugEnabled(),
|
||||
{
|
||||
// default value before is fetched
|
||||
fallbackData: false,
|
||||
},
|
||||
);
|
||||
|
||||
// 处理流量数据更新 - 使用节流控制更新频率
|
||||
const handleTrafficUpdate = useCallback((event: MessageEvent) => {
|
||||
@@ -260,14 +257,23 @@ export const EnhancedTrafficStats = () => {
|
||||
const { server, secret = "" } = clashInfo;
|
||||
if (!server) return;
|
||||
|
||||
// WebSocket 引用
|
||||
let sockets: {
|
||||
traffic: ReturnType<typeof createAuthSockette> | null;
|
||||
memory: ReturnType<typeof createAuthSockette> | null;
|
||||
} = {
|
||||
traffic: null,
|
||||
memory: null,
|
||||
};
|
||||
|
||||
// 清理现有连接的函数
|
||||
const cleanupSockets = () => {
|
||||
Object.values(socketRefs.current).forEach((socket) => {
|
||||
Object.values(sockets).forEach((socket) => {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
}
|
||||
});
|
||||
socketRefs.current = { traffic: null, memory: null };
|
||||
sockets = { traffic: null, memory: null };
|
||||
};
|
||||
|
||||
// 关闭现有连接
|
||||
@@ -277,10 +283,7 @@ export const EnhancedTrafficStats = () => {
|
||||
console.log(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`,
|
||||
);
|
||||
socketRefs.current.traffic = createAuthSockette(
|
||||
`${server}/traffic`,
|
||||
secret,
|
||||
{
|
||||
sockets.traffic = createAuthSockette(`${server}/traffic`, secret, {
|
||||
onmessage: handleTrafficUpdate,
|
||||
onopen: (event) => {
|
||||
console.log(
|
||||
@@ -308,13 +311,12 @@ export const EnhancedTrafficStats = () => {
|
||||
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`,
|
||||
);
|
||||
socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, {
|
||||
sockets.memory = createAuthSockette(`${server}/memory`, secret, {
|
||||
onmessage: handleMemoryUpdate,
|
||||
onopen: (event) => {
|
||||
console.log(
|
||||
@@ -353,18 +355,6 @@ export const EnhancedTrafficStats = () => {
|
||||
return cleanupSockets;
|
||||
}, [clashInfo, pageVisible, handleTrafficUpdate, handleMemoryUpdate]);
|
||||
|
||||
// 组件卸载时清理所有定时器/引用
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
try {
|
||||
Object.values(socketRefs.current).forEach((socket) => {
|
||||
if (socket) socket.close();
|
||||
});
|
||||
socketRefs.current = { traffic: null, memory: null };
|
||||
} catch {}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 执行垃圾回收
|
||||
const handleGarbageCollection = useCallback(async () => {
|
||||
if (isDebug) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import useSWRSubscription from "swr/subscription";
|
||||
import { createAuthSockette } from "@/utils/websocket";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isDebugEnabled, gc } from "@/services/api";
|
||||
import useSWR from "swr";
|
||||
|
||||
interface MemoryUsage {
|
||||
inuse: number;
|
||||
@@ -31,12 +32,15 @@ export const LayoutTraffic = () => {
|
||||
|
||||
const trafficRef = useRef<TrafficRef>(null);
|
||||
const pageVisible = useVisibility();
|
||||
const [isDebug, setIsDebug] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
isDebugEnabled().then((flag) => setIsDebug(flag));
|
||||
return () => {};
|
||||
}, [isDebug]);
|
||||
const { data: isDebug } = useSWR(
|
||||
"clash-verge-rev-internal://isDebugEnabled",
|
||||
() => isDebugEnabled(),
|
||||
{
|
||||
// default value before is fetched
|
||||
fallbackData: false,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: traffic = { up: 0, down: 0 } } = useSWRSubscription<
|
||||
ITrafficItem,
|
||||
|
||||
@@ -48,6 +48,10 @@ import MonacoEditor from "react-monaco-editor";
|
||||
import { useThemeMode } from "@/services/states";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import {
|
||||
requestIdleCallback,
|
||||
cancelIdleCallback,
|
||||
} from "foxact/request-idle-callback";
|
||||
|
||||
interface Props {
|
||||
proxiesUid: string;
|
||||
@@ -195,11 +199,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
// 防止异常导致UI卡死
|
||||
}
|
||||
};
|
||||
if (window.requestIdleCallback) {
|
||||
window.requestIdleCallback(serialize);
|
||||
} else {
|
||||
setTimeout(serialize, 0);
|
||||
}
|
||||
|
||||
const handle = requestIdleCallback(serialize);
|
||||
return () => {
|
||||
cancelIdleCallback(handle);
|
||||
};
|
||||
}
|
||||
}, [prependSeq, appendSeq, deleteSeq]);
|
||||
|
||||
|
||||
@@ -480,8 +480,11 @@ export const ProxyGroups = (props: Props) => {
|
||||
}
|
||||
}, [handleWheel]);
|
||||
|
||||
// 监听窗口大小变化
|
||||
// layout effect runs before paint
|
||||
useEffect(() => {
|
||||
// 添加窗口大小变化监听和最大高度计算
|
||||
const updateMaxHeight = useCallback(() => {
|
||||
const updateMaxHeight = () => {
|
||||
if (!alphabetSelectorRef.current) return;
|
||||
|
||||
const windowHeight = window.innerHeight;
|
||||
@@ -495,16 +498,16 @@ export const ProxyGroups = (props: Props) => {
|
||||
alphabetSelectorRef.current.style.top = `calc(48% + ${offsetPercentage}vh)`;
|
||||
|
||||
setMaxHeight(`${availableHeight}px`);
|
||||
}, []);
|
||||
};
|
||||
|
||||
// 监听窗口大小变化
|
||||
useEffect(() => {
|
||||
updateMaxHeight();
|
||||
|
||||
window.addEventListener("resize", updateMaxHeight);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateMaxHeight);
|
||||
};
|
||||
}, [updateMaxHeight]);
|
||||
}, []);
|
||||
|
||||
if (mode === "direct") {
|
||||
return <BaseEmpty text={t("clash_mode_direct")} />;
|
||||
|
||||
@@ -110,7 +110,8 @@ export const useRenderList = (mode: string) => {
|
||||
(mode === "rule" && !groups.length) ||
|
||||
(mode === "global" && proxies.length < 2)
|
||||
) {
|
||||
setTimeout(() => refreshProxy(), 500);
|
||||
const handle = setTimeout(() => refreshProxy(), 500);
|
||||
return () => clearTimeout(handle);
|
||||
}
|
||||
}, [proxiesData, mode, refreshProxy]);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BaseDialog, DialogRef } from "@/components/base";
|
||||
@@ -30,7 +30,6 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([]);
|
||||
const [dataSource, setDataSource] = useState<BackupFile[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
@@ -91,14 +90,14 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
.sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDataSource(
|
||||
const dataSource = useMemo<BackupFile[]>(
|
||||
() =>
|
||||
backupFiles.slice(
|
||||
page * DEFAULT_ROWS_PER_PAGE,
|
||||
page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE,
|
||||
),
|
||||
[backupFiles, page],
|
||||
);
|
||||
}, [page, backupFiles]);
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
@@ -116,18 +115,10 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
<Paper elevation={2} sx={{ padding: 2 }}>
|
||||
<BackupConfigViewer
|
||||
setLoading={setIsLoading}
|
||||
onBackupSuccess={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onSaveSuccess={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onRefresh={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onInit={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onBackupSuccess={fetchAndSetBackupFiles}
|
||||
onSaveSuccess={fetchAndSetBackupFiles}
|
||||
onRefresh={fetchAndSetBackupFiles}
|
||||
onInit={fetchAndSetBackupFiles}
|
||||
/>
|
||||
<Divider sx={{ marginY: 2 }} />
|
||||
<BackupTableViewer
|
||||
|
||||
@@ -6,13 +6,11 @@ import { alpha, Box, Button, IconButton } from "@mui/material";
|
||||
import { ContentCopyRounded } from "@mui/icons-material";
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import useSWR from "swr";
|
||||
|
||||
export const NetworkInterfaceViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [networkInterfaces, setNetworkInterfaces] = useState<
|
||||
INetworkInterface[]
|
||||
>([]);
|
||||
const [isV4, setIsV4] = useState(true);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
@@ -22,12 +20,13 @@ export const NetworkInterfaceViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
getNetworkInterfacesInfo().then((res) => {
|
||||
setNetworkInterfaces(res);
|
||||
});
|
||||
}, [open]);
|
||||
const { data: networkInterfaces } = useSWR(
|
||||
"clash-verge-rev-internal://network-interfaces",
|
||||
getNetworkInterfacesInfo,
|
||||
{
|
||||
fallbackData: [], // default data before fetch
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
|
||||
Reference in New Issue
Block a user