refactor: migrate query cache to swr

This commit is contained in:
Tunglies
2026-06-28 19:39:15 +08:00
parent 294955cb8c
commit e71e701fd0
18 changed files with 213 additions and 58 deletions
+1 -1
View File
@@ -43,7 +43,6 @@
"@monaco-editor/react": "^4.7.0",
"@mui/icons-material": "^9.0.1",
"@mui/material": "^9.0.1",
"@tanstack/react-query": "^5.100.14",
"@tanstack/react-virtual": "^3.13.25",
"@tauri-apps/api": "2.11.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
@@ -74,6 +73,7 @@
"react-markdown": "10.1.0",
"react-router": "^8.0.0",
"rehype-raw": "^7.0.0",
"swr": "^2.4.2",
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo",
"types-pac": "^1.0.3",
"validator": "^13.15.35"
+14 -18
View File
@@ -35,9 +35,6 @@ importers:
'@mui/material':
specifier: ^9.0.1
version: 9.0.1(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@tanstack/react-query':
specifier: ^5.100.14
version: 5.100.14(react@19.2.7)
'@tanstack/react-virtual':
specifier: ^3.13.25
version: 3.13.25(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
@@ -128,6 +125,9 @@ importers:
rehype-raw:
specifier: ^7.0.0
version: 7.0.0
swr:
specifier: ^2.4.2
version: 2.4.2(react@19.2.7)
tauri-plugin-mihomo-api:
specifier: github:clash-verge-rev/tauri-plugin-mihomo
version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/ff75ee3edd61923e6c76a7f3cf3c3fbd0f8e22c0
@@ -1475,14 +1475,6 @@ packages:
peerDependencies:
'@svgr/core': '*'
'@tanstack/query-core@5.100.14':
resolution: {integrity: sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==}
'@tanstack/react-query@5.100.14':
resolution: {integrity: sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==}
peerDependencies:
react: ^18 || ^19
'@tanstack/react-virtual@3.13.25':
resolution: {integrity: sha512-bmNoqMu6gcAW9JGrKVB0Q1tN1i5RONZF8r1fW0bbE4Oyf3DwEGnzzQJ2OW+Ozg1P4s8PyugkHg2ULZoFQN+cqw==}
peerDependencies:
@@ -3349,6 +3341,11 @@ packages:
svg-parser@2.0.4:
resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==}
swr@2.4.2:
resolution: {integrity: sha512-ej644Y2bvkIajfR32KGeSSdBXQW+ScjGjkybZgSE7kFpk9eGnV44XY9FJylXi+W75pavSX1PVNB57W5EbhGIYw==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
systemjs@6.15.1:
resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==}
@@ -4992,13 +4989,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@tanstack/query-core@5.100.14': {}
'@tanstack/react-query@5.100.14(react@19.2.7)':
dependencies:
'@tanstack/query-core': 5.100.14
react: 19.2.7
'@tanstack/react-virtual@3.13.25(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
'@tanstack/virtual-core': 3.15.0
@@ -7032,6 +7022,12 @@ snapshots:
svg-parser@2.0.4: {}
swr@2.4.2(react@19.2.7):
dependencies:
dequal: 2.0.3
react: 19.2.7
use-sync-external-store: 1.6.0(react@19.2.7)
systemjs@6.15.1: {}
tar@7.5.15:
+1 -1
View File
@@ -5,7 +5,6 @@ import {
VisibilityOutlined,
} from '@mui/icons-material'
import { Box, Button, IconButton, Skeleton, Typography } from '@mui/material'
import { useQuery } from '@tanstack/react-query'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { useEffect } from 'foxact/use-abortable-effect'
import { useIntersection } from 'foxact/use-intersection'
@@ -21,6 +20,7 @@ import {
import { useTranslation } from 'react-i18next'
import { getIpInfo } from '@/services/api'
import { useQuery } from '@/services/query-client'
import { EnhancedCard } from './enhanced-card'
+1 -1
View File
@@ -10,7 +10,6 @@ import {
Typography,
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { useQuery } from '@tanstack/react-query'
import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual'
import { useLockFn } from 'ahooks'
import {
@@ -34,6 +33,7 @@ import { useVerge } from '@/hooks/use-verge'
import { useProxiesData } from '@/providers/app-data-context'
import { calcuProxies, updateProxyChainConfigInRuntime } from '@/services/cmds'
import delayManager from '@/services/delay'
import { useQuery } from '@/services/query-client'
import { debugLog } from '@/utils/debug'
import { ScrollTopButton } from '../layout/scroll-top-button'
+1 -1
View File
@@ -1,4 +1,3 @@
import { useQuery } from '@tanstack/react-query'
import { useLockFn } from 'ahooks'
import { getVersion } from 'tauri-plugin-mihomo-api'
@@ -8,6 +7,7 @@ import {
getRuntimeConfig,
patchClashConfig,
} from '@/services/cmds'
import { useQuery } from '@/services/query-client'
import { queryClient } from '@/services/query-client'
type MutateClashUpdater =
+1 -1
View File
@@ -1,8 +1,8 @@
import { useQuery } from '@tanstack/react-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import { useMemo } from 'react'
import { downloadIconCache } from '@/services/cmds'
import { useQuery } from '@/services/query-client'
export interface UseIconCacheOptions {
icon?: string | null
+1 -2
View File
@@ -1,9 +1,9 @@
import { useQueryClient } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { useEffect, useRef } from 'react'
import { MihomoWebSocket, type LogLevel } from 'tauri-plugin-mihomo-api'
import { getClashLogs } from '@/services/cmds'
import { queryClient } from '@/services/query-client'
import { useClashLog } from './use-clash-log'
import { useMihomoWsSubscription } from './use-mihomo-ws-subscription'
@@ -48,7 +48,6 @@ const appendLogs = (
}
export const useLogData = () => {
const queryClient = useQueryClient()
const [clashLog] = useClashLog()
const enableLog = clashLog.enable
const logLevel = clashLog.logLevel.toUpperCase() as LogLevel
+4 -5
View File
@@ -1,8 +1,9 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useLocalStorage } from 'foxact/use-local-storage'
import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api'
import { queryClient, useQuery } from '@/services/query-client'
export const RECONNECT_DELAY_MS = 1000
interface SharedSubscriptionOwner {
@@ -195,8 +196,6 @@ export const useMihomoWsSubscription = <T>(
const responseCacheKey =
subscriptionCacheKey ?? lastSubscriptionCacheKeyRef.current
const queryClient = useQueryClient()
const wsRef = useRef<MihomoWebSocket | null>(null)
const resolveNextData = useCallback(
@@ -211,7 +210,7 @@ export const useMihomoWsSubscription = <T>(
}
return data ?? fallbackData
},
[queryClient, fallbackData],
[fallbackData],
)
const response = useQuery<T>({
@@ -355,7 +354,7 @@ export const useMihomoWsSubscription = <T>(
queryClient.removeQueries({ queryKey: [subscriptionCacheKey] })
}
setDate(Date.now())
}, [queryClient, subscriptionCacheKey, setDate])
}, [subscriptionCacheKey, setDate])
return { response, refresh, subscriptionCacheKey: responseCacheKey, wsRef }
}
+1 -2
View File
@@ -1,6 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import { getNetworkInterfacesInfo } from '@/services/cmds'
import { useQuery } from '@/services/query-client'
export const useNetworkInterfaces = () => {
const {
+1 -1
View File
@@ -1,4 +1,3 @@
import { useQuery } from '@tanstack/react-query'
import { selectNodeForGroup } from 'tauri-plugin-mihomo-api'
import {
@@ -7,6 +6,7 @@ import {
patchProfile,
patchProfilesConfig,
} from '@/services/cmds'
import { useQuery } from '@/services/query-client'
import { queryClient } from '@/services/query-client'
import { debugLog } from '@/utils/debug'
+1 -1
View File
@@ -1,10 +1,10 @@
import { useQuery } from '@tanstack/react-query'
import { useRef } from 'react'
import { closeAllConnections } from 'tauri-plugin-mihomo-api'
import { useVerge } from '@/hooks/use-verge'
import { useClashConfigData, useSystemData } from '@/providers/app-data-context'
import { getAutotemProxy } from '@/services/cmds'
import { useQuery } from '@/services/query-client'
import { queryClient } from '@/services/query-client'
// 系统代理状态检测统一逻辑
+1 -1
View File
@@ -1,8 +1,8 @@
import { useQuery } from '@tanstack/react-query'
import { useEffect, useRef, useState } from 'react'
import { getRunningMode, isAdmin, isServiceAvailable } from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
import { useQuery } from '@/services/query-client'
import { useVerge } from './use-verge'
+1 -2
View File
@@ -1,5 +1,4 @@
import { useQuery } from '@tanstack/react-query'
import { useQuery } from '@/services/query-client'
import { queryClient } from '@/services/query-client'
import { checkUpdateSafe } from '@/services/update'
+4 -5
View File
@@ -1,11 +1,10 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useCallback } from 'react'
import { getVergeConfig, patchVergeConfig } from '@/services/cmds'
import { getPreloadConfig, setPreloadConfig } from '@/services/preload'
import { queryClient, useQuery } from '@/services/query-client'
export const useVerge = () => {
const qc = useQueryClient()
const initialVergeConfig = getPreloadConfig()
const { data: verge, refetch } = useQuery({
@@ -31,11 +30,11 @@ export const useVerge = () => {
return
}
if (typeof updaterOrData === 'function') {
const prev = qc.getQueryData<IVergeConfig>(['getVergeConfig'])
const prev = queryClient.getQueryData<IVergeConfig>(['getVergeConfig'])
const next = updaterOrData(prev)
qc.setQueryData(['getVergeConfig'], next)
queryClient.setQueryData(['getVergeConfig'], next)
} else {
qc.setQueryData(['getVergeConfig'], updaterOrData)
queryClient.setQueryData(['getVergeConfig'], updaterOrData)
}
}
+4 -4
View File
@@ -1,11 +1,11 @@
import './assets/styles/index.scss'
import { ResizeObserver } from '@juggle/resize-observer'
import { QueryClientProvider } from '@tanstack/react-query'
import { ComposeContextProvider } from 'foxact/compose-context-provider'
import React from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider } from 'react-router'
import { SWRConfig } from 'swr'
import { MihomoWebSocket } from 'tauri-plugin-mihomo-api'
import { BaseErrorBoundary } from './components/base'
@@ -18,7 +18,7 @@ import {
resolveThemeMode,
getPreloadConfig,
} from './services/preload'
import { queryClient } from './services/query-client'
import { queryClientConfig } from './services/query-client'
import {
LoadingCacheProvider,
ThemeModeProvider,
@@ -51,13 +51,13 @@ const initializeApp = (initialThemeMode: 'light' | 'dark') => {
<React.StrictMode>
<ComposeContextProvider contexts={contexts}>
<BaseErrorBoundary>
<QueryClientProvider client={queryClient}>
<SWRConfig value={queryClientConfig}>
<WindowProvider>
<AppDataProvider>
<RouterProvider router={router} />
</AppDataProvider>
</WindowProvider>
</QueryClientProvider>
</SWRConfig>
</BaseErrorBoundary>
</ComposeContextProvider>
</React.StrictMode>,
+1 -1
View File
@@ -21,7 +21,6 @@ import {
TextSnippetOutlined,
} from '@mui/icons-material'
import { Box, Button, Divider, Grid, IconButton, Stack } from '@mui/material'
import { useQuery } from '@tanstack/react-query'
import { TauriEvent } from '@tauri-apps/api/event'
import { readText } from '@tauri-apps/plugin-clipboard-manager'
import { readTextFile } from '@tauri-apps/plugin-fs'
@@ -65,6 +64,7 @@ import {
updateProfile,
} from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
import { useQuery } from '@/services/query-client'
import { queryClient } from '@/services/query-client'
import {
useLoadingCache,
+1 -1
View File
@@ -1,4 +1,3 @@
import { useQuery } from '@tanstack/react-query'
import { listen } from '@tauri-apps/api/event'
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import {
@@ -15,6 +14,7 @@ import {
getRunningMode,
getSystemProxy,
} from '@/services/cmds'
import { useQuery } from '@/services/query-client'
import { queryClient } from '@/services/query-client'
import {
+174 -10
View File
@@ -1,12 +1,176 @@
import { QueryClient } from '@tanstack/react-query'
import { unstable_serialize } from 'swr'
import useSWR, {
type SWRConfiguration,
type SWRResponse,
mutate as swrMutate,
} from 'swr'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 2000,
retry: 3,
retryDelay: 5000,
refetchOnWindowFocus: false,
},
type QueryKey = string | readonly unknown[]
type QueryOptions<T> = {
queryKey: QueryKey
queryFn: () => Promise<T> | T
enabled?: boolean
initialData?: T | (() => T | undefined)
placeholderData?: T | (() => T | undefined)
staleTime?: number
gcTime?: number
retry?: number | false
retryDelay?: number | ((attempt: number) => number)
refetchInterval?: number | false
refetchIntervalInBackground?: boolean
refetchOnWindowFocus?: boolean
refetchOnReconnect?: boolean
}
type QueryResult<T> = Omit<SWRResponse<T>, 'mutate'> & {
isFetching: boolean
isPending: boolean
refetch: () => Promise<{ data: T | undefined }>
mutate: SWRResponse<T>['mutate']
}
type QueryFilters = {
queryKey: QueryKey
}
const keyToSWRKey = (queryKey: QueryKey) => {
return Array.isArray(queryKey) ? [...queryKey] : queryKey
}
const serializeQueryKey = (queryKey: QueryKey) => {
return unstable_serialize(keyToSWRKey(queryKey))
}
const resolveFallbackData = <T>(
initialData: QueryOptions<T>['initialData'],
placeholderData: QueryOptions<T>['placeholderData'],
) => {
const data = initialData ?? placeholderData
return typeof data === 'function' ? (data as () => T | undefined)() : data
}
const resolveRetryInterval = (
retryDelay: QueryOptions<unknown>['retryDelay'],
) => {
if (typeof retryDelay !== 'function') return retryDelay
return retryDelay(0)
}
export const queryCache = new Map<string, unknown>()
export const queryClientConfig: SWRConfiguration = {
dedupingInterval: 2000,
errorRetryCount: 3,
errorRetryInterval: 5000,
revalidateOnFocus: false,
}
export const queryClient = {
getQueryData<T>(queryKey: QueryKey): T | undefined {
return queryCache.get(serializeQueryKey(queryKey)) as T | undefined
},
})
setQueryData<T>(
queryKey: QueryKey,
updaterOrData: T | undefined | ((current: T | undefined) => T | undefined),
) {
const current = this.getQueryData<T>(queryKey)
const next =
typeof updaterOrData === 'function'
? (updaterOrData as (current: T | undefined) => T | undefined)(current)
: updaterOrData
if (next === undefined) {
queryCache.delete(serializeQueryKey(queryKey))
} else {
queryCache.set(serializeQueryKey(queryKey), next)
}
void swrMutate(keyToSWRKey(queryKey), next, {
populateCache: true,
revalidate: false,
})
return next
},
invalidateQueries({ queryKey }: QueryFilters) {
return swrMutate(keyToSWRKey(queryKey))
},
removeQueries({ queryKey }: QueryFilters) {
queryCache.delete(serializeQueryKey(queryKey))
return swrMutate(keyToSWRKey(queryKey), undefined, {
populateCache: true,
revalidate: false,
})
},
async fetchQuery<T>({
queryKey,
queryFn,
}: {
queryKey: QueryKey
queryFn: () => Promise<T> | T
}) {
const data = await queryFn()
this.setQueryData(queryKey, data)
return data
},
}
export const invalidateQueries = (queryKeys: readonly QueryKey[]) =>
Promise.all(
queryKeys.map((queryKey) => queryClient.invalidateQueries({ queryKey })),
)
export function useQuery<T>(options: QueryOptions<T>): QueryResult<T> {
const {
queryKey,
queryFn,
enabled = true,
initialData,
placeholderData,
retry,
retryDelay,
refetchInterval,
refetchIntervalInBackground,
refetchOnWindowFocus,
refetchOnReconnect,
staleTime,
} = options
const fallbackData = resolveFallbackData(initialData, placeholderData)
const serializedKey = serializeQueryKey(queryKey)
if (enabled && fallbackData !== undefined && !queryCache.has(serializedKey)) {
queryCache.set(serializedKey, fallbackData)
}
const swr = useSWR<T>(enabled ? keyToSWRKey(queryKey) : null, queryFn, {
dedupingInterval: staleTime,
errorRetryCount: retry === false ? 0 : retry,
errorRetryInterval: resolveRetryInterval(retryDelay),
fallbackData,
keepPreviousData: placeholderData !== undefined,
revalidateOnFocus: refetchOnWindowFocus,
revalidateOnReconnect: refetchOnReconnect,
refreshInterval: refetchInterval || 0,
refreshWhenHidden: refetchIntervalInBackground ?? false,
onSuccess: (data) => {
queryCache.set(serializedKey, data)
},
})
return {
...swr,
isFetching: swr.isValidating,
isPending: swr.isLoading,
refetch: async () => {
const data = await swr.mutate()
if (data !== undefined) {
queryCache.set(serializedKey, data)
}
return { data }
},
}
}