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
@@ -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 }
},
}
}