Explorar el Código

refactor: replace recoil (#1137)

Sukka hace 1 año
padre
commit
66dd510acc

+ 1 - 1
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"

+ 37 - 33
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

+ 3 - 3
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!)

+ 3 - 4
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;

+ 2 - 3
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<any>();
   const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null);
-  const themeMode = useRecoilValue(atomThemeMode);
+  const themeMode = useThemeMode();
 
   useEffect(() => {
     if (!open) return;

+ 3 - 3
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<any>(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;
 

+ 2 - 3
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("");

+ 4 - 3
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<DialogRef>((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,

+ 16 - 3
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 = [
+  <ThemeModeProvider />,
+  <LogDataProvider />,
+  <LoadingCacheProvider />,
+  <UpdateStateProvider />,
+];
+
 createRoot(container).render(
   <React.StrictMode>
-    <RecoilRoot>
+    <ComposeContextProvider contexts={contexts}>
       <BaseErrorBoundary>
         <BrowserRouter>
           <Layout />
         </BrowserRouter>
       </BaseErrorBoundary>
-    </RecoilRoot>
+    </ComposeContextProvider>
   </React.StrictMode>
 );

+ 2 - 3
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();

+ 8 - 5
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<IConnections>(initConn);
 
-  const [setting, setSetting] = useRecoilState(atomConnectionSetting);
+  const [setting, setSetting] = useConnectionSetting();
 
-  const isTableLayout = setting.layout === "table";
+  const isTableLayout =
+    (setting || defaultConnectionSetting).layout === "table";
 
   const orderOpts: Record<string, OrderFunc> = {
     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" }
               )

+ 4 - 4
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");

+ 3 - 6
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)"

+ 2 - 3
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 (

+ 38 - 60
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<ILogItem[]>({
-  key: "atomLogData",
-  default: [],
-});
+const [LogDataProvider, useLogData, useSetLogData] = createContextState<
+  ILogItem[]
+>([]);
 
-export const atomEnableLog = atom<boolean>({
-  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<IConnectionSetting>({
-  key: "atomConnectionSetting",
-  effects: [
-    ({ setSelf, onSet }) => {
-      const key = "connections-setting";
-
-      try {
-        const value = localStorage.getItem(key);
-        const data = value == null ? { layout: "table" } : JSON.parse(value);
-        setSelf(data);
-      } catch {
-        setSelf({ layout: "table" });
-      }
+export const defaultConnectionSetting: IConnectionSetting = { layout: "table" };
 
-      onSet((newValue) => {
-        try {
-          localStorage.setItem(key, JSON.stringify(newValue));
-        } catch {}
-      });
-    },
-  ],
-});
+export const useConnectionSetting = () =>
+  useLocalStorage<IConnectionSetting>(
+    "connections-setting",
+    defaultConnectionSetting,
+    {
+      serializer: JSON.stringify,
+      deserializer: JSON.parse,
+    }
+  );
 
 // save the state of each profile item loading
-export const atomLoadingCache = atom<Record<string, boolean>>({
-  key: "atomLoadingCache",
-  default: {},
-});
+const [LoadingCacheProvider, useLoadingCache, useSetLoadingCache] =
+  createContextState<Record<string, boolean>>({});
 
 // save update state
-export const atomUpdateState = atom<boolean>({
-  key: "atomUpdateState",
-  default: false,
-});
+const [UpdateStateProvider, useUpdateState, useSetUpdateState] =
+  createContextState<boolean>(false);
+
+export {
+  ThemeModeProvider,
+  useThemeMode,
+  useSetThemeMode,
+  LogDataProvider,
+  useLogData,
+  useSetLogData,
+  LoadingCacheProvider,
+  useLoadingCache,
+  useSetLoadingCache,
+  UpdateStateProvider,
+  useUpdateState,
+  useSetUpdateState,
+};