From 66dd510acc3be4f666beb3726c954cd15525ac26 Mon Sep 17 00:00:00 2001 From: Sukka Date: Fri, 7 Jun 2024 12:27:37 +0800 Subject: [PATCH] refactor: replace recoil (#1137) --- package.json | 2 +- pnpm-lock.yaml | 70 ++++++------- src/components/layout/use-custom-theme.ts | 6 +- src/components/layout/use-log-setup.ts | 7 +- src/components/profile/editor-viewer.tsx | 5 +- src/components/profile/profile-item.tsx | 6 +- src/components/proxy/proxy-render.tsx | 5 +- src/components/setting/mods/update-viewer.tsx | 7 +- src/main.tsx | 19 +++- src/pages/_layout.tsx | 5 +- src/pages/connections.tsx | 13 ++- src/pages/logs.tsx | 8 +- src/pages/profiles.tsx | 9 +- src/pages/settings.tsx | 5 +- src/services/states.ts | 98 +++++++------------ 15 files changed, 128 insertions(+), 137 deletions(-) diff --git a/package.json b/package.json index 1c6a0e3..99fe357 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "ahooks": "^3.8.0", "axios": "^1.7.2", "dayjs": "1.11.5", + "foxact": "^0.2.34", "i18next": "^23.11.5", "lodash-es": "^4.17.21", "meta-json-schema": "1.18.5-alpha4", @@ -48,7 +49,6 @@ "react-router-dom": "^6.23.1", "react-transition-group": "^4.4.5", "react-virtuoso": "^4.7.11", - "recoil": "^0.7.7", "swr": "^1.3.0", "tar": "^6.2.1", "types-pac": "^1.0.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c66c97e..00a9147 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,6 +52,9 @@ importers: dayjs: specifier: 1.11.5 version: 1.11.5 + foxact: + specifier: ^0.2.34 + version: 0.2.34(react@18.3.1) i18next: specifier: ^23.11.5 version: 23.11.5 @@ -97,9 +100,6 @@ importers: react-virtuoso: specifier: ^4.7.11 version: 4.7.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - recoil: - specifier: ^0.7.7 - version: 0.7.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) swr: specifier: ^1.3.0 version: 1.3.0(react@18.3.1) @@ -2526,6 +2526,12 @@ packages: } engines: { node: ">=10" } + client-only@0.0.1: + resolution: + { + integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==, + } + clsx@2.1.1: resolution: { @@ -2836,6 +2842,17 @@ packages: } engines: { node: ">=12.20.0" } + foxact@0.2.34: + resolution: + { + integrity: sha512-9GrB4NPhTjaJ5pzMkfYFatLGgt5LWq3hhVhYR7zG/PaHhtt3ObOzdRVmmO/whh5E7W8JBykiS6RLtnjeLZLSeg==, + } + peerDependencies: + react: "*" + peerDependenciesMeta: + react: + optional: true + fs-extra@11.2.0: resolution: { @@ -2898,12 +2915,6 @@ packages: integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, } - hamt_plus@1.0.2: - resolution: - { - integrity: sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==, - } - has-flag@3.0.0: resolution: { @@ -3876,21 +3887,6 @@ packages: } engines: { node: ">=8.10.0" } - recoil@0.7.7: - resolution: - { - integrity: sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==, - } - peerDependencies: - react: ">=16.13.1" - react-dom: "*" - react-native: "*" - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - regenerate-unicode-properties@10.1.1: resolution: { @@ -4004,6 +4000,12 @@ packages: } hasBin: true + server-only@0.0.1: + resolution: + { + integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==, + } + shebang-command@2.0.0: resolution: { @@ -6054,6 +6056,8 @@ snapshots: chownr@2.0.0: {} + client-only@0.0.1: {} + clsx@2.1.1: {} color-convert@1.9.3: @@ -6233,6 +6237,13 @@ snapshots: dependencies: fetch-blob: 3.2.0 + foxact@0.2.34(react@18.3.1): + dependencies: + client-only: 0.0.1 + server-only: 0.0.1 + optionalDependencies: + react: 18.3.1 + fs-extra@11.2.0: dependencies: graceful-fs: 4.2.11 @@ -6262,8 +6273,6 @@ snapshots: graceful-fs@4.2.11: {} - hamt_plus@1.0.2: {} - has-flag@3.0.0: {} hasown@2.0.2: @@ -6914,13 +6923,6 @@ snapshots: dependencies: picomatch: 2.3.1 - recoil@0.7.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - hamt_plus: 1.0.2 - react: 18.3.1 - optionalDependencies: - react-dom: 18.3.1(react@18.3.1) - regenerate-unicode-properties@10.1.1: dependencies: regenerate: 1.4.2 @@ -7011,6 +7013,8 @@ snapshots: semver@6.3.1: {} + server-only@0.0.1: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/src/components/layout/use-custom-theme.ts b/src/components/layout/use-custom-theme.ts index aa8ef94..99879a7 100644 --- a/src/components/layout/use-custom-theme.ts +++ b/src/components/layout/use-custom-theme.ts @@ -1,8 +1,7 @@ import { useEffect, useMemo } from "react"; -import { useRecoilState } from "recoil"; import { alpha, createTheme, Shadows, Theme } from "@mui/material"; import { appWindow } from "@tauri-apps/api/window"; -import { atomThemeMode } from "@/services/states"; +import { useSetThemeMode, useThemeMode } from "@/services/states"; import { defaultTheme, defaultDarkTheme } from "@/pages/_theme"; import { useVerge } from "@/hooks/use-verge"; @@ -12,7 +11,8 @@ import { useVerge } from "@/hooks/use-verge"; export const useCustomTheme = () => { const { verge } = useVerge(); const { theme_mode, theme_setting } = verge ?? {}; - const [mode, setMode] = useRecoilState(atomThemeMode); + const mode = useThemeMode(); + const setMode = useSetThemeMode(); useEffect(() => { const themeMode = ["light", "dark", "system"].includes(theme_mode!) diff --git a/src/components/layout/use-log-setup.ts b/src/components/layout/use-log-setup.ts index 0c130ac..4a1e567 100644 --- a/src/components/layout/use-log-setup.ts +++ b/src/components/layout/use-log-setup.ts @@ -1,9 +1,8 @@ import dayjs from "dayjs"; import { useEffect } from "react"; -import { useRecoilValue, useSetRecoilState } from "recoil"; import { getClashLogs } from "@/services/cmds"; import { useClashInfo } from "@/hooks/use-clash"; -import { atomEnableLog, atomLogData } from "@/services/states"; +import { useEnableLog, useSetLogData } from "@/services/states"; import { useWebsocket } from "@/hooks/use-websocket"; const MAX_LOG_NUM = 1000; @@ -12,8 +11,8 @@ const MAX_LOG_NUM = 1000; export const useLogSetup = () => { const { clashInfo } = useClashInfo(); - const enableLog = useRecoilValue(atomEnableLog); - const setLogData = useSetRecoilState(atomLogData); + const [enableLog] = useEnableLog(); + const setLogData = useSetLogData(); const { connect, disconnect } = useWebsocket((event) => { const data = JSON.parse(event.data) as ILogItem; diff --git a/src/components/profile/editor-viewer.tsx b/src/components/profile/editor-viewer.tsx index d898230..d97e6ef 100644 --- a/src/components/profile/editor-viewer.tsx +++ b/src/components/profile/editor-viewer.tsx @@ -1,6 +1,5 @@ import { ReactNode, useEffect, useRef } from "react"; import { useLockFn } from "ahooks"; -import { useRecoilValue } from "recoil"; import { useTranslation } from "react-i18next"; import { Button, @@ -9,7 +8,7 @@ import { DialogContent, DialogTitle, } from "@mui/material"; -import { atomThemeMode } from "@/services/states"; +import { useThemeMode } from "@/services/states"; import { readProfileFile, saveProfileFile } from "@/services/cmds"; import { Notice } from "@/components/base"; import { nanoid } from "nanoid"; @@ -90,7 +89,7 @@ export const EditorViewer = (props: Props) => { const { t } = useTranslation(); const editorRef = useRef(); const instanceRef = useRef(null); - const themeMode = useRecoilValue(atomThemeMode); + const themeMode = useThemeMode(); useEffect(() => { if (!open) return; diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index dc00843..bf3edf0 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -2,7 +2,6 @@ import dayjs from "dayjs"; import { mutate } from "swr"; import { useEffect, useState } from "react"; import { useLockFn } from "ahooks"; -import { useRecoilState } from "recoil"; import { useTranslation } from "react-i18next"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; @@ -17,7 +16,7 @@ import { CircularProgress, } from "@mui/material"; import { RefreshRounded, DragIndicator } from "@mui/icons-material"; -import { atomLoadingCache } from "@/services/states"; +import { useLoadingCache, useSetLoadingCache } from "@/services/states"; import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds"; import { Notice } from "@/components/base"; import { EditorViewer } from "@/components/profile/editor-viewer"; @@ -47,7 +46,8 @@ export const ProfileItem = (props: Props) => { const { t } = useTranslation(); const [anchorEl, setAnchorEl] = useState(null); const [position, setPosition] = useState({ left: 0, top: 0 }); - const [loadingCache, setLoadingCache] = useRecoilState(atomLoadingCache); + const loadingCache = useLoadingCache(); + const setLoadingCache = useSetLoadingCache(); const { uid, name = "Profile", extra, updated = 0 } = itemData; diff --git a/src/components/proxy/proxy-render.tsx b/src/components/proxy/proxy-render.tsx index 8f8b927..68faaa1 100644 --- a/src/components/proxy/proxy-render.tsx +++ b/src/components/proxy/proxy-render.tsx @@ -17,8 +17,7 @@ import { ProxyItem } from "./proxy-item"; import { ProxyItemMini } from "./proxy-item-mini"; import type { IRenderItem } from "./use-render-list"; import { useVerge } from "@/hooks/use-verge"; -import { useRecoilState } from "recoil"; -import { atomThemeMode } from "@/services/states"; +import { useThemeMode } from "@/services/states"; import { useEffect, useMemo, useState } from "react"; import { convertFileSrc } from "@tauri-apps/api/tauri"; import { downloadIconCache } from "@/services/cmds"; @@ -38,7 +37,7 @@ export const ProxyRender = (props: RenderProps) => { const { type, group, headState, proxy, proxyCol } = item; const { verge } = useVerge(); const enable_group_icon = verge?.enable_group_icon ?? true; - const [mode] = useRecoilState(atomThemeMode); + const mode = useThemeMode(); const isDark = mode === "light" ? false : true; const itembackgroundcolor = isDark ? "#282A36" : "#ffffff"; const [iconCachePath, setIconCachePath] = useState(""); diff --git a/src/components/setting/mods/update-viewer.tsx b/src/components/setting/mods/update-viewer.tsx index ca66e16..40a10dd 100644 --- a/src/components/setting/mods/update-viewer.tsx +++ b/src/components/setting/mods/update-viewer.tsx @@ -2,12 +2,11 @@ import useSWR from "swr"; import { forwardRef, useImperativeHandle, useState, useMemo } from "react"; import { useLockFn } from "ahooks"; import { Box, LinearProgress } from "@mui/material"; -import { useRecoilState } from "recoil"; import { useTranslation } from "react-i18next"; import { relaunch } from "@tauri-apps/api/process"; import { checkUpdate, installUpdate } from "@tauri-apps/api/updater"; import { BaseDialog, DialogRef, Notice } from "@/components/base"; -import { atomUpdateState } from "@/services/states"; +import { useUpdateState, useSetUpdateState } from "@/services/states"; import { listen, Event, UnlistenFn } from "@tauri-apps/api/event"; import { portableFlag } from "@/pages/_layout"; import ReactMarkdown from "react-markdown"; @@ -18,7 +17,9 @@ export const UpdateViewer = forwardRef((props, ref) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const [updateState, setUpdateState] = useRecoilState(atomUpdateState); + + const updateState = useUpdateState(); + const setUpdateState = useSetUpdateState(); const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, { errorRetryCount: 2, diff --git a/src/main.tsx b/src/main.tsx index c64dd23..b66f7d6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,11 +9,17 @@ if (!window.ResizeObserver) { import React from "react"; import { createRoot } from "react-dom/client"; -import { RecoilRoot } from "recoil"; +import { ComposeContextProvider } from "foxact/compose-context-provider"; import { BrowserRouter } from "react-router-dom"; import { BaseErrorBoundary } from "./components/base"; import Layout from "./pages/_layout"; import "./services/i18n"; +import { + LoadingCacheProvider, + LogDataProvider, + ThemeModeProvider, + UpdateStateProvider, +} from "./services/states"; const mainElementId = "root"; const container = document.getElementById(mainElementId); @@ -37,14 +43,21 @@ document.addEventListener("keydown", (event) => { } }); +const contexts = [ + , + , + , + , +]; + createRoot(container).render( - + - + ); diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 196bc43..836c7ad 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -14,8 +14,7 @@ import { useVerge } from "@/hooks/use-verge"; import LogoSvg from "@/assets/image/logo.svg?react"; import iconLight from "@/assets/image/icon_light.svg?react"; import iconDark from "@/assets/image/icon_dark.svg?react"; -import { atomThemeMode } from "@/services/states"; -import { useRecoilState } from "recoil"; +import { useThemeMode } from "@/services/states"; import { Notice } from "@/components/base"; import { LayoutItem } from "@/components/layout/layout-item"; import { LayoutControl } from "@/components/layout/layout-control"; @@ -36,7 +35,7 @@ dayjs.extend(relativeTime); const OS = getSystem(); const Layout = () => { - const [mode] = useRecoilState(atomThemeMode); + const mode = useThemeMode(); const isDark = mode === "light" ? false : true; const { t } = useTranslation(); const { theme } = useCustomTheme(); diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index 8494427..2b219b5 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -1,12 +1,14 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useLockFn } from "ahooks"; import { Box, Button, IconButton, MenuItem } from "@mui/material"; -import { useRecoilState } from "recoil"; import { Virtuoso } from "react-virtuoso"; import { useTranslation } from "react-i18next"; import { TableChartRounded, TableRowsRounded } from "@mui/icons-material"; import { closeAllConnections } from "@/services/api"; -import { atomConnectionSetting } from "@/services/states"; +import { + defaultConnectionSetting, + useConnectionSetting, +} from "@/services/states"; import { useClashInfo } from "@/hooks/use-clash"; import { BaseEmpty, BasePage } from "@/components/base"; import { useWebsocket } from "@/hooks/use-websocket"; @@ -34,9 +36,10 @@ const ConnectionsPage = () => { const [curOrderOpt, setOrderOpt] = useState("Default"); const [connData, setConnData] = useState(initConn); - const [setting, setSetting] = useRecoilState(atomConnectionSetting); + const [setting, setSetting] = useConnectionSetting(); - const isTableLayout = setting.layout === "table"; + const isTableLayout = + (setting || defaultConnectionSetting).layout === "table"; const orderOpts: Record = { Default: (list) => @@ -137,7 +140,7 @@ const ConnectionsPage = () => { size="small" onClick={() => setSetting((o) => - o.layout === "list" + o?.layout !== "table" ? { ...o, layout: "table" } : { ...o, layout: "list" } ) diff --git a/src/pages/logs.tsx b/src/pages/logs.tsx index e76a5fc..267cfa9 100644 --- a/src/pages/logs.tsx +++ b/src/pages/logs.tsx @@ -1,5 +1,4 @@ import { useMemo, useState } from "react"; -import { useRecoilState } from "recoil"; import { Box, Button, IconButton, MenuItem } from "@mui/material"; import { Virtuoso } from "react-virtuoso"; import { useTranslation } from "react-i18next"; @@ -7,7 +6,7 @@ import { PlayCircleOutlineRounded, PauseCircleOutlineRounded, } from "@mui/icons-material"; -import { atomEnableLog, atomLogData } from "@/services/states"; +import { useEnableLog, useLogData, useSetLogData } from "@/services/states"; import { BaseEmpty, BasePage } from "@/components/base"; import LogItem from "@/components/log/log-item"; import { useCustomTheme } from "@/components/layout/use-custom-theme"; @@ -16,8 +15,9 @@ import { BaseStyledSelect } from "@/components/base/base-styled-select"; const LogPage = () => { const { t } = useTranslation(); - const [logData, setLogData] = useRecoilState(atomLogData); - const [enableLog, setEnableLog] = useRecoilState(atomEnableLog); + const logData = useLogData(); + const setLogData = useSetLogData(); + const [enableLog, setEnableLog] = useEnableLog(); const { theme } = useCustomTheme(); const isDark = theme.palette.mode === "dark"; const [logState, setLogState] = useState("all"); diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 9ca2596..be92cc3 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -1,7 +1,6 @@ import useSWR, { mutate } from "swr"; import { useEffect, useMemo, useRef, useState } from "react"; import { useLockFn } from "ahooks"; -import { useSetRecoilState } from "recoil"; import { Box, Button, Grid, IconButton, Stack, Divider } from "@mui/material"; import { DndContext, @@ -35,7 +34,7 @@ import { reorderProfile, createProfile, } from "@/services/cmds"; -import { atomLoadingCache } from "@/services/states"; +import { useSetLoadingCache, useThemeMode } from "@/services/states"; import { closeAllConnections } from "@/services/api"; import { BasePage, DialogRef, Notice } from "@/components/base"; import { @@ -47,8 +46,6 @@ import { ProfileMore } from "@/components/profile/profile-more"; import { useProfiles } from "@/hooks/use-profiles"; import { ConfigViewer } from "@/components/setting/mods/config-viewer"; import { throttle } from "lodash-es"; -import { useRecoilState } from "recoil"; -import { atomThemeMode } from "@/services/states"; import { BaseStyledTextField } from "@/components/base/base-styled-text-field"; import { listen } from "@tauri-apps/api/event"; import { readTextFile } from "@tauri-apps/api/fs"; @@ -239,7 +236,7 @@ const ProfilePage = () => { }); // 更新所有订阅 - const setLoadingCache = useSetRecoilState(atomLoadingCache); + const setLoadingCache = useSetLoadingCache(); const onUpdateAll = useLockFn(async () => { const throttleMutate = throttle(mutateProfiles, 2000, { trailing: true, @@ -271,7 +268,7 @@ const ProfilePage = () => { const text = await readText(); if (text) setUrl(text); }; - const [mode] = useRecoilState(atomThemeMode); + const mode = useThemeMode(); const islight = mode === "light" ? true : false; const dividercolor = islight ? "rgba(0, 0, 0, 0.06)" diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index d89cf14..adf7538 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -7,8 +7,7 @@ import { openWebUrl } from "@/services/cmds"; import SettingVerge from "@/components/setting/setting-verge"; import SettingClash from "@/components/setting/setting-clash"; import SettingSystem from "@/components/setting/setting-system"; -import { atomThemeMode } from "@/services/states"; -import { useRecoilState } from "recoil"; +import { useThemeMode } from "@/services/states"; const SettingPage = () => { const { t } = useTranslation(); @@ -25,7 +24,7 @@ const SettingPage = () => { return openWebUrl("https://clash-verge-rev.github.io/guide/log.html"); }); - const [mode] = useRecoilState(atomThemeMode); + const mode = useThemeMode(); const isDark = mode === "light" ? false : true; return ( diff --git a/src/services/states.ts b/src/services/states.ts index 6ac6edc..bfb34be 100644 --- a/src/services/states.ts +++ b/src/services/states.ts @@ -1,73 +1,51 @@ -import { atom } from "recoil"; +import { createContextState } from "foxact/create-context-state"; +import { useLocalStorage } from "foxact/use-local-storage"; -export const atomThemeMode = atom<"light" | "dark">({ - key: "atomThemeMode", - default: "light", -}); +const [ThemeModeProvider, useThemeMode, useSetThemeMode] = createContextState< + "light" | "dark" +>("light"); -export const atomLogData = atom({ - key: "atomLogData", - default: [], -}); +const [LogDataProvider, useLogData, useSetLogData] = createContextState< + ILogItem[] +>([]); -export const atomEnableLog = atom({ - key: "atomEnableLog", - effects: [ - ({ setSelf, onSet }) => { - const key = "enable-log"; - - try { - setSelf(localStorage.getItem(key) !== "false"); - } catch {} - - onSet((newValue, _, isReset) => { - try { - if (isReset) { - localStorage.removeItem(key); - } else { - localStorage.setItem(key, newValue.toString()); - } - } catch {} - }); - }, - ], -}); +export const useEnableLog = () => useLocalStorage("enable-log", true); interface IConnectionSetting { layout: "table" | "list"; } -export const atomConnectionSetting = atom({ - key: "atomConnectionSetting", - effects: [ - ({ setSelf, onSet }) => { - const key = "connections-setting"; +export const defaultConnectionSetting: IConnectionSetting = { layout: "table" }; - try { - const value = localStorage.getItem(key); - const data = value == null ? { layout: "table" } : JSON.parse(value); - setSelf(data); - } catch { - setSelf({ layout: "table" }); - } - - onSet((newValue) => { - try { - localStorage.setItem(key, JSON.stringify(newValue)); - } catch {} - }); - }, - ], -}); +export const useConnectionSetting = () => + useLocalStorage( + "connections-setting", + defaultConnectionSetting, + { + serializer: JSON.stringify, + deserializer: JSON.parse, + } + ); // save the state of each profile item loading -export const atomLoadingCache = atom>({ - key: "atomLoadingCache", - default: {}, -}); +const [LoadingCacheProvider, useLoadingCache, useSetLoadingCache] = + createContextState>({}); // save update state -export const atomUpdateState = atom({ - key: "atomUpdateState", - default: false, -}); +const [UpdateStateProvider, useUpdateState, useSetUpdateState] = + createContextState(false); + +export { + ThemeModeProvider, + useThemeMode, + useSetThemeMode, + LogDataProvider, + useLogData, + useSetLogData, + LoadingCacheProvider, + useLoadingCache, + useSetLoadingCache, + UpdateStateProvider, + useUpdateState, + useSetUpdateState, +};