From 6e0647f7b2d16947c7d4b295c2711ec7097fa63b Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 29 Apr 2026 01:00:13 +0800 Subject: [PATCH] perf: narrow app data subscriptions --- src/components/home/clash-info-card.tsx | 12 +++- src/components/home/clash-mode-card.tsx | 10 ++- src/components/home/current-proxy-card.tsx | 15 ++++- src/components/home/home-profile-card.tsx | 4 +- src/components/proxy/provider-button.tsx | 5 +- src/components/proxy/proxy-chain.tsx | 5 +- src/components/proxy/proxy-groups.tsx | 6 +- src/components/proxy/use-render-list.ts | 5 +- src/components/rule/provider-button.tsx | 5 +- .../setting/mods/sysproxy-viewer.tsx | 6 +- .../setting/mods/tunnels-viewer.tsx | 4 +- src/hooks/use-current-proxy.ts | 10 ++- src/hooks/use-system-proxy-state.ts | 5 +- src/pages/proxies.tsx | 8 ++- src/pages/rules.tsx | 5 +- src/providers/app-data-context.ts | 62 ++++++++++++++++--- src/providers/app-data-provider.tsx | 14 ++++- 17 files changed, 136 insertions(+), 45 deletions(-) diff --git a/src/components/home/clash-info-card.tsx b/src/components/home/clash-info-card.tsx index 6115abdcc..fdd63eb63 100644 --- a/src/components/home/clash-info-card.tsx +++ b/src/components/home/clash-info-card.tsx @@ -4,7 +4,12 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useClash } from '@/hooks/use-clash' -import { useAppData } from '@/providers/app-data-context' +import { + useClashConfigData, + useRulesData, + useSystemData, + useUptimeData, +} from '@/providers/app-data-context' import { EnhancedCard } from './enhanced-card' @@ -19,7 +24,10 @@ const formatUptime = (uptimeMs: number) => { export const ClashInfoCard = () => { const { t } = useTranslation() const { version: clashVersion } = useClash() - const { clashConfig, rules, uptime, systemProxyAddress } = useAppData() + const { clashConfig } = useClashConfigData() + const { rules } = useRulesData() + const { uptime } = useUptimeData() + const { systemProxyAddress } = useSystemData() // 使用useMemo缓存格式化后的uptime,避免频繁计算 const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]) diff --git a/src/components/home/clash-mode-card.tsx b/src/components/home/clash-mode-card.tsx index d346a1421..94225672b 100644 --- a/src/components/home/clash-mode-card.tsx +++ b/src/components/home/clash-mode-card.tsx @@ -10,7 +10,11 @@ import { useTranslation } from 'react-i18next' import { closeAllConnections } from 'tauri-plugin-mihomo-api' import { useVerge } from '@/hooks/use-verge' -import { useAppData } from '@/providers/app-data-context' +import { + useAppRefreshers, + useClashConfigData, + useCoreDataStatus, +} from '@/providers/app-data-context' import { patchClashMode } from '@/services/cmds' import type { TranslationKey } from '@/types/generated/i18n-keys' @@ -41,7 +45,9 @@ const MODE_META: Record< export const ClashModeCard = () => { const { t } = useTranslation() const { verge } = useVerge() - const { clashConfig, isCoreDataPending, refreshClashConfig } = useAppData() + const { clashConfig } = useClashConfigData() + const { isCoreDataPending } = useCoreDataStatus() + const { refreshClashConfig } = useAppRefreshers() // 支持的模式列表 const modeList = CLASH_MODES diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index e7f1b20a3..3a28991c2 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -37,7 +37,13 @@ import { EnhancedCard } from '@/components/home/enhanced-card' import { useProfiles } from '@/hooks/use-profiles' import { useProxySelection } from '@/hooks/use-proxy-selection' import { useVerge } from '@/hooks/use-verge' -import { useAppData } from '@/providers/app-data-context' +import { + useAppRefreshers, + useClashConfigData, + useCoreDataStatus, + useProxiesData, + useRulesData, +} from '@/providers/app-data-context' import delayManager from '@/services/delay' import { debugLog } from '@/utils/debug' @@ -105,8 +111,11 @@ export const CurrentProxyCard = () => { const { t } = useTranslation() const navigate = useNavigate() const theme = useTheme() - const { proxies, clashConfig, isCoreDataPending, refreshProxy, rules } = - useAppData() + const { proxies } = useProxiesData() + const { clashConfig } = useClashConfigData() + const { rules } = useRulesData() + const { refreshProxy } = useAppRefreshers() + const { isCoreDataPending } = useCoreDataStatus() const { verge } = useVerge() const { current: currentProfile } = useProfiles() const autoDelayEnabled = verge?.enable_auto_delay_detection ?? false diff --git a/src/components/home/home-profile-card.tsx b/src/components/home/home-profile-card.tsx index b87e4af88..51db674a7 100644 --- a/src/components/home/home-profile-card.tsx +++ b/src/components/home/home-profile-card.tsx @@ -24,7 +24,7 @@ import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router' -import { useAppData } from '@/providers/app-data-context' +import { useAppRefreshers } from '@/providers/app-data-context' import { openWebUrl, updateProfile } from '@/services/cmds' import { showNotice } from '@/services/notice-service' import parseTraffic from '@/utils/parse-traffic' @@ -281,7 +281,7 @@ export const HomeProfileCard = ({ }: HomeProfileCardProps) => { const { t } = useTranslation() const navigate = useNavigate() - const { refreshAll } = useAppData() + const { refreshAll } = useAppRefreshers() // 更新当前订阅 const [updating, setUpdating] = useState(false) diff --git a/src/components/proxy/provider-button.tsx b/src/components/proxy/provider-button.tsx index d71154e42..e3ac500b1 100644 --- a/src/components/proxy/provider-button.tsx +++ b/src/components/proxy/provider-button.tsx @@ -22,7 +22,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { updateProxyProvider } from 'tauri-plugin-mihomo-api' -import { useAppData } from '@/providers/app-data-context' +import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context' import { showNotice } from '@/services/notice-service' import parseTraffic from '@/utils/parse-traffic' @@ -48,7 +48,8 @@ const parseExpire = (expire?: number) => { export const ProviderButton = () => { const { t } = useTranslation() const [open, setOpen] = useState(false) - const { proxyProviders, refreshProxy, refreshProxyProviders } = useAppData() + const { proxyProviders } = useProxiesData() + const { refreshProxy, refreshProxyProviders } = useAppRefreshers() const [updating, setUpdating] = useState>({}) // 检查是否有提供者 diff --git a/src/components/proxy/proxy-chain.tsx b/src/components/proxy/proxy-chain.tsx index 682b557f5..cef8ffcf9 100644 --- a/src/components/proxy/proxy-chain.tsx +++ b/src/components/proxy/proxy-chain.tsx @@ -40,7 +40,7 @@ import { selectNodeForGroup, } from 'tauri-plugin-mihomo-api' -import { useAppData } from '@/providers/app-data-context' +import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context' import { updateProxyChainConfigInRuntime } from '@/services/cmds' import { debugLog } from '@/utils/debug' @@ -251,7 +251,8 @@ export const ProxyChain = ({ }: ProxyChainProps) => { const theme = useTheme() const { t } = useTranslation() - const { proxies, refreshProxy } = useAppData() + const { proxies } = useProxiesData() + const { refreshProxy } = useAppRefreshers() const [isConnecting, setIsConnecting] = useState(false) const markUnsavedChanges = useCallback(() => { onMarkUnsavedChanges?.() diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx index f502859e0..1561c77d0 100644 --- a/src/components/proxy/proxy-groups.tsx +++ b/src/components/proxy/proxy-groups.tsx @@ -19,7 +19,7 @@ import { delayGroup, healthcheckProxyProvider } from 'tauri-plugin-mihomo-api' import { BaseEmpty } from '@/components/base' import { useProxySelection } from '@/hooks/use-proxy-selection' import { useVerge } from '@/hooks/use-verge' -import { useAppData } from '@/providers/app-data-context' +import { useProxiesData } from '@/providers/app-data-context' import { calcuProxies, updateProxyChainConfigInRuntime } from '@/services/cmds' import delayManager from '@/services/delay' import { debugLog } from '@/utils/debug' @@ -57,7 +57,7 @@ export const ProxyGroups = (props: Props) => { const { t } = useTranslation() const { mode, isChainMode = false, chainConfigData } = props - // Drive 3s polling on the shared TQ cache; data is read via useAppData() below + // Drive 3s polling on the shared TQ cache; data is read via granular context below useQuery({ queryKey: ['getProxies'], queryFn: calcuProxies, @@ -95,7 +95,7 @@ export const ProxyGroups = (props: Props) => { }>({ open: false, message: '' }) const { verge } = useVerge() - const { proxies: proxiesData } = useAppData() + const { proxies: proxiesData } = useProxiesData() const groups = proxiesData?.groups const availableGroups = useMemo(() => { if (!groups) return [] diff --git a/src/components/proxy/use-render-list.ts b/src/components/proxy/use-render-list.ts index 5064d7eec..8814213a3 100644 --- a/src/components/proxy/use-render-list.ts +++ b/src/components/proxy/use-render-list.ts @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef } from 'react' import { useRuntimeConfig } from '@/hooks/use-clash' import { useVerge } from '@/hooks/use-verge' -import { useAppData } from '@/providers/app-data-context' +import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context' import delayManager from '@/services/delay' import { debugLog } from '@/utils/debug' @@ -108,7 +108,8 @@ export const useRenderList = ( selectedGroup?: string | null, ) => { // 使用全局数据提供者 - const { proxies: proxiesData, refreshProxy } = useAppData() + const { proxies: proxiesData } = useProxiesData() + const { refreshProxy } = useAppRefreshers() const { verge } = useVerge() const { width } = useWindowWidth() const [headStates, setHeadState] = useHeadStateNew() diff --git a/src/components/rule/provider-button.tsx b/src/components/rule/provider-button.tsx index bed1e5b3c..ac24167c5 100644 --- a/src/components/rule/provider-button.tsx +++ b/src/components/rule/provider-button.tsx @@ -21,7 +21,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { updateRuleProvider } from 'tauri-plugin-mihomo-api' -import { useAppData } from '@/providers/app-data-context' +import { useAppRefreshers, useRulesData } from '@/providers/app-data-context' import { showNotice } from '@/services/notice-service' // 辅助组件 - 类型框 @@ -40,7 +40,8 @@ const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({ export const ProviderButton = () => { const { t } = useTranslation() const [open, setOpen] = useState(false) - const { ruleProviders, refreshRules, refreshRuleProviders } = useAppData() + const { ruleProviders } = useRulesData() + const { refreshRules, refreshRuleProviders } = useAppRefreshers() const [updating, setUpdating] = useState>({}) // 检查是否有提供者 diff --git a/src/components/setting/mods/sysproxy-viewer.tsx b/src/components/setting/mods/sysproxy-viewer.tsx index cf1ec3dc3..7465870a2 100644 --- a/src/components/setting/mods/sysproxy-viewer.tsx +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -34,7 +34,7 @@ import { import { EditorViewer } from '@/components/profile/editor-viewer' import { useSystemProxyState } from '@/hooks/use-system-proxy-state' import { useVerge } from '@/hooks/use-verge' -import { useAppData } from '@/providers/app-data-context' +import { useClashConfigData, useSystemData } from '@/providers/app-data-context' import { getAutotemProxy, getNetworkInterfacesInfo, @@ -108,7 +108,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { const { verge, patchVerge, mutateVerge } = useVerge() const [hostOptions, setHostOptions] = useState([]) - const { clashConfig } = useAppData() + const { clashConfig } = useClashConfigData() const { indicator: isProxyReallyEnabled, invalidateProxyState } = useSystemProxyState() @@ -179,7 +179,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { updateProxy() }, [clashConfig?.mixedPort, enabled, value.pac, invalidateProxyState]) - const { systemProxyAddress } = useAppData() + const { systemProxyAddress } = useSystemData() // 为当前状态计算系统代理地址 const getSystemProxyAddress = useMemo(() => { diff --git a/src/components/setting/mods/tunnels-viewer.tsx b/src/components/setting/mods/tunnels-viewer.tsx index 9e0b2499d..03f8b38ad 100644 --- a/src/components/setting/mods/tunnels-viewer.tsx +++ b/src/components/setting/mods/tunnels-viewer.tsx @@ -16,7 +16,7 @@ import { useTranslation } from 'react-i18next' import { BaseDialog } from '@/components/base' import { useClash } from '@/hooks/use-clash' -import { useAppData } from '@/providers/app-data-context' +import { useProxiesData } from '@/providers/app-data-context' import { isPortInUse } from '@/services/cmds' import { showNotice } from '@/services/notice-service' import { @@ -92,7 +92,7 @@ export const TunnelsViewer = forwardRef((_, ref) => { }) }, [draftTunnels]) - const { proxies } = useAppData() + const { proxies } = useProxiesData() const proxyGroups = useMemo(() => { return proxies?.groups ?? [] diff --git a/src/hooks/use-current-proxy.ts b/src/hooks/use-current-proxy.ts index c5d7d0996..08dc905c3 100644 --- a/src/hooks/use-current-proxy.ts +++ b/src/hooks/use-current-proxy.ts @@ -1,6 +1,10 @@ import { useMemo } from 'react' -import { useAppData } from '@/providers/app-data-context' +import { + useAppRefreshers, + useClashConfigData, + useProxiesData, +} from '@/providers/app-data-context' // 定义代理组类型 interface ProxyGroup { @@ -11,7 +15,9 @@ interface ProxyGroup { // 获取当前代理节点信息的自定义Hook export const useCurrentProxy = () => { // 从AppDataProvider获取数据 - const { proxies, clashConfig, refreshProxy } = useAppData() + const { proxies } = useProxiesData() + const { clashConfig } = useClashConfigData() + const { refreshProxy } = useAppRefreshers() // 获取当前模式 const currentMode = clashConfig?.mode?.toLowerCase() || 'rule' diff --git a/src/hooks/use-system-proxy-state.ts b/src/hooks/use-system-proxy-state.ts index 8b62c4aec..308dc6b35 100644 --- a/src/hooks/use-system-proxy-state.ts +++ b/src/hooks/use-system-proxy-state.ts @@ -3,14 +3,15 @@ import { useRef } from 'react' import { closeAllConnections } from 'tauri-plugin-mihomo-api' import { useVerge } from '@/hooks/use-verge' -import { useAppData } from '@/providers/app-data-context' +import { useClashConfigData, useSystemData } from '@/providers/app-data-context' import { getAutotemProxy } from '@/services/cmds' import { queryClient } from '@/services/query-client' // 系统代理状态检测统一逻辑 export const useSystemProxyState = () => { const { verge, mutateVerge, patchVerge } = useVerge() - const { sysproxy, clashConfig } = useAppData() + const { sysproxy } = useSystemData() + const { clashConfig } = useClashConfigData() const { data: autoproxy } = useQuery({ queryKey: ['getAutotemProxy'], queryFn: getAutotemProxy, diff --git a/src/pages/proxies.tsx b/src/pages/proxies.tsx index bcda76e03..a7fd369f2 100644 --- a/src/pages/proxies.tsx +++ b/src/pages/proxies.tsx @@ -9,7 +9,10 @@ import { BasePage } from '@/components/base' import { ProviderButton } from '@/components/proxy/provider-button' import { ProxyGroups } from '@/components/proxy/proxy-groups' import { useVerge } from '@/hooks/use-verge' -import { useAppData } from '@/providers/app-data-context' +import { + useAppRefreshers, + useClashConfigData, +} from '@/providers/app-data-context' import { getRuntimeProxyChainConfig, patchClashMode, @@ -41,7 +44,8 @@ const ProxyPage = () => { null as string | null, ) - const { clashConfig, refreshClashConfig } = useAppData() + const { clashConfig } = useClashConfigData() + const { refreshClashConfig } = useAppRefreshers() const updateChainConfigData = useCallback((value: string | null) => { dispatchChainConfigData(value) diff --git a/src/pages/rules.tsx b/src/pages/rules.tsx index f341ab39a..8e5eab613 100644 --- a/src/pages/rules.tsx +++ b/src/pages/rules.tsx @@ -13,11 +13,12 @@ import { ScrollTopButton } from '@/components/layout/scroll-top-button' import { ProviderButton } from '@/components/rule/provider-button' import RuleItem from '@/components/rule/rule-item' import { useVisibility } from '@/hooks/use-visibility' -import { useAppData } from '@/providers/app-data-context' +import { useAppRefreshers, useRulesData } from '@/providers/app-data-context' const RulesPage = () => { const { t } = useTranslation() - const { rules = [], refreshRules, refreshRuleProviders } = useAppData() + const { rules = [] } = useRulesData() + const { refreshRules, refreshRuleProviders } = useAppRefreshers() const [match, setMatch] = useState(() => (_: string) => true) const virtuosoRef = useRef(null) const [showScrollTop, setShowScrollTop] = useState(false) diff --git a/src/providers/app-data-context.ts b/src/providers/app-data-context.ts index c8ba000e8..2a09d284b 100644 --- a/src/providers/app-data-context.ts +++ b/src/providers/app-data-context.ts @@ -65,6 +65,10 @@ export interface UptimeContextType { uptime: number } +export interface CoreDataStatusContextType { + isCoreDataPending: boolean +} + export interface RefreshersContextType { refreshProxy: () => Promise refreshClashConfig: () => Promise @@ -82,23 +86,63 @@ export const ClashConfigContext = createContext( ) export const SystemContext = createContext(null) export const UptimeContext = createContext(null) +export const CoreDataStatusContext = + createContext(null) export const RefreshersContext = createContext( null, ) -const useCtx = (ctx: Context): T => { +const useCtx = (ctx: Context, hookName: string): T => { const v = use(ctx) - if (!v) throw new Error('useAppData must be used within AppDataProvider') + if (!v) throw new Error(`${hookName} must be used within AppDataProvider`) return v } +export const useProxiesData = () => { + const { proxies, proxyProviders, isProxiesPending } = useCtx( + ProxiesContext, + 'useProxiesData', + ) + + return { + proxies, + proxyProviders: proxyProviders as Record, + isProxiesPending, + } +} + +export const useRulesData = () => { + const { rules, ruleProviders } = useCtx(RulesContext, 'useRulesData') + + return { + rules, + ruleProviders: ruleProviders as Record, + } +} + +export const useClashConfigData = (): ClashConfigContextType => + useCtx(ClashConfigContext, 'useClashConfigData') + +export const useSystemData = (): SystemContextType => + useCtx(SystemContext, 'useSystemData') + +export const useUptimeData = (): UptimeContextType => + useCtx(UptimeContext, 'useUptimeData') + +export const useAppRefreshers = (): RefreshersContextType => + useCtx(RefreshersContext, 'useAppRefreshers') + +export const useCoreDataStatus = (): CoreDataStatusContextType => + useCtx(CoreDataStatusContext, 'useCoreDataStatus') + export const useAppData = (): AppDataContextType => { - const { proxies, proxyProviders, isProxiesPending } = useCtx(ProxiesContext) - const { rules, ruleProviders } = useCtx(RulesContext) - const { clashConfig, isClashConfigPending } = useCtx(ClashConfigContext) - const { sysproxy, runningMode, systemProxyAddress } = useCtx(SystemContext) - const { uptime } = useCtx(UptimeContext) - const refreshers = useCtx(RefreshersContext) + const { proxies, proxyProviders } = useProxiesData() + const { rules, ruleProviders } = useRulesData() + const { clashConfig } = useClashConfigData() + const { sysproxy, runningMode, systemProxyAddress } = useSystemData() + const { uptime } = useUptimeData() + const { isCoreDataPending } = useCoreDataStatus() + const refreshers = useAppRefreshers() return { proxies, @@ -110,7 +154,7 @@ export const useAppData = (): AppDataContextType => { proxyProviders: proxyProviders as Record, ruleProviders: ruleProviders as Record, systemProxyAddress, - isCoreDataPending: isProxiesPending || isClashConfigPending, + isCoreDataPending, ...refreshers, } } diff --git a/src/providers/app-data-provider.tsx b/src/providers/app-data-provider.tsx index d3b19c145..d788370a5 100644 --- a/src/providers/app-data-provider.tsx +++ b/src/providers/app-data-provider.tsx @@ -18,6 +18,7 @@ import { import { ClashConfigContext, + CoreDataStatusContext, ProxiesContext, RefreshersContext, RulesContext, @@ -265,6 +266,11 @@ export const AppDataProvider = ({ const uptimeValue = useMemo(() => ({ uptime: uptimeData || 0 }), [uptimeData]) + const coreDataStatusValue = useMemo( + () => ({ isCoreDataPending: isProxiesPending || isClashConfigPending }), + [isProxiesPending, isClashConfigPending], + ) + const refreshersValue = useMemo( () => ({ refreshProxy, @@ -292,9 +298,11 @@ export const AppDataProvider = ({ - - {children} - + + + {children} + +