diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 338fd6b2e7c..173a983eca6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -264,9 +264,6 @@ catalogs: cli-table3: specifier: 0.6.5 version: 0.6.5 - client-only: - specifier: 0.0.1 - version: 0.0.1 clsx: specifier: 2.1.1 version: 2.1.1 @@ -348,6 +345,9 @@ catalogs: fast-deep-equal: specifier: 3.1.3 version: 3.1.3 + foxact: + specifier: 0.3.4 + version: 0.3.4 fuse.js: specifier: 7.4.2 version: 7.4.2 @@ -1089,9 +1089,6 @@ importers: class-variance-authority: specifier: 'catalog:' version: 0.7.1 - client-only: - specifier: 'catalog:' - version: 0.0.1 cmdk: specifier: 'catalog:' version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1134,6 +1131,9 @@ importers: fast-deep-equal: specifier: 'catalog:' version: 3.1.3 + foxact: + specifier: 'catalog:' + version: 0.3.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) fuse.js: specifier: 'catalog:' version: 7.4.2 @@ -6358,6 +6358,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-target-bus@1.0.0: + resolution: {integrity: sha512-uPcWKbj/BJU3Tbw9XqhHqET4/LBOhvv3/SJWr7NksxA6TC5YqBpaZgawE9R+WpYFCBFSAE4Vun+xQS6w4ABdlA==} + eventsource-parser@3.1.0: resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} engines: {node: '>=18.0.0'} @@ -6463,6 +6466,17 @@ packages: engines: {node: '>=18.3.0'} hasBin: true + foxact@0.3.4: + resolution: {integrity: sha512-6ENWStzEp/VczVgwBdn8scZUGccko2kLGho8Qg1VyUTG6tpSW1vz2xX/dRtAUjPEFbu618DKsjj/YL7iMU8mlA==} + peerDependencies: + react: '*' + react-dom: '*' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -14740,6 +14754,8 @@ snapshots: esutils@2.0.3: {} + event-target-bus@1.0.0: {} + eventsource-parser@3.1.0: {} expand-template@2.0.3: @@ -14843,6 +14859,15 @@ snapshots: dependencies: fd-package-json: 2.0.0 + foxact@0.3.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + client-only: 0.0.1 + event-target-bus: 1.0.0 + server-only: 0.0.1 + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + fs-constants@1.0.0: optional: true @@ -18169,7 +18194,6 @@ time: chokidar@5.0.0: '2025-11-25T23:28:06.854Z' class-variance-authority@0.7.1: '2024-11-26T08:20:34.604Z' cli-table3@0.6.5: '2024-05-12T16:36:50.079Z' - client-only@0.0.1: '2022-09-03T01:07:11.981Z' clsx@2.1.1: '2024-04-23T05:26:04.645Z' cmdk@1.1.1: '2025-03-14T19:21:16.194Z' code-inspector-plugin@1.5.2: '2026-06-07T02:32:04.545Z' @@ -18197,6 +18221,7 @@ time: eslint@10.4.1: '2026-05-29T20:32:32.193Z' eventsource-parser@3.1.0: '2026-05-27T20:55:51.466Z' fast-deep-equal@3.1.3: '2020-06-08T07:27:28.474Z' + foxact@0.3.4: '2026-05-15T12:55:52.584Z' fuse.js@7.4.2: '2026-06-05T22:22:52.388Z' happy-dom@20.10.2: '2026-06-06T15:03:11.325Z' hast-util-to-jsx-runtime@2.3.6: '2025-03-05T11:30:29.166Z' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4a9100a24e5..8bd3056bf23 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -131,7 +131,6 @@ catalog: chokidar: 5.0.0 class-variance-authority: 0.7.1 cli-table3: 0.6.5 - client-only: 0.0.1 clsx: 2.1.1 cmdk: 1.1.1 code-inspector-plugin: 1.5.2 @@ -159,6 +158,7 @@ catalog: eslint-plugin-storybook: 10.4.2 eventsource-parser: 3.1.0 fast-deep-equal: 3.1.3 + foxact: 0.3.4 fuse.js: 7.4.2 happy-dom: 20.10.2 hast-util-to-jsx-runtime: 2.3.6 diff --git a/web/AGENTS.md b/web/AGENTS.md index 8dc91be1a2d..21def787b9e 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -33,7 +33,7 @@ - Use local component state for state owned by one component. - Use feature-level Jotai atoms for simple client state shared across components in the same feature, especially when components need a shared source of truth, derived values, or shared actions. - Use existing feature stores for complex or high-frequency interaction state such as workflow canvas, drag, resize, and panel runtime state. -- Use `@/hooks/use-local-storage` only for low-frequency, client-only persistence such as user preferences, dismissed notices, and UI defaults. Do not use localStorage as the live source of truth for app state. +- Use `foxact/use-local-storage` only for low-frequency, client-only persistence such as user preferences, dismissed notices, and UI defaults. Do not use localStorage as the live source of truth for app state. - For high-frequency interactions, update the feature state during interaction and persist storage only on commit or settled updates. - Do not access `localStorage`, `window.localStorage`, or `globalThis.localStorage` directly in app code; use the storage hook boundary and preserve existing raw/custom storage formats. - Do not add ad hoc global event listeners for shared state. Prefer atoms, existing stores, or a shared subscription hook so listeners are centralized and deduplicated. diff --git a/web/__tests__/billing/education-verification-flow.test.tsx b/web/__tests__/billing/education-verification-flow.test.tsx index 6c7f05c4df7..83885b9f9f9 100644 --- a/web/__tests__/billing/education-verification-flow.test.tsx +++ b/web/__tests__/billing/education-verification-flow.test.tsx @@ -77,7 +77,7 @@ vi.mock('@/hooks/use-async-window-open', () => ({ useAsyncWindowOpen: () => vi.fn(), })) -vi.mock('@/hooks/use-local-storage', () => ({ +vi.mock('foxact/use-local-storage', () => ({ useSetLocalStorage: () => mockSetEducationVerifying, })) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx index 3e73e210eb4..06465e3551a 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -6,6 +6,7 @@ import type { UpdateAppSiteCodeResponse } from '@/models/app' import type { App } from '@/types/app' import type { I18nKeysByPrefix } from '@/types/i18n' import { toast } from '@langgenius/dify-ui/toast' +import { useSetLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -18,7 +19,6 @@ import { collaborationManager } from '@/app/components/workflow/collaboration/co import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager' import { isTriggerNode } from '@/app/components/workflow/types' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { fetchAppDetail, updateAppSiteAccessToken, diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index c455cf7ab3d..e725ac5e5f2 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -11,6 +11,7 @@ import { RiFocus2Fill, RiFocus2Line, } from '@remixicon/react' +import { useLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -24,7 +25,6 @@ import DatasetDetailContext from '@/context/dataset-detail' import { useEventEmitterContextContext } from '@/context/event-emitter' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useDocumentTitle from '@/hooks/use-document-title' -import { useLocalStorage } from '@/hooks/use-local-storage' import { usePathname, useRouter } from '@/next/navigation' import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset' diff --git a/web/app/components/__tests__/education-verify-action-recorder.spec.tsx b/web/app/components/__tests__/education-verify-action-recorder.spec.tsx index 742fb9c2493..d93dc90c2ac 100644 --- a/web/app/components/__tests__/education-verify-action-recorder.spec.tsx +++ b/web/app/components/__tests__/education-verify-action-recorder.spec.tsx @@ -12,7 +12,7 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: vi.fn(), })) -vi.mock('@/hooks/use-local-storage', () => ({ +vi.mock('foxact/use-local-storage', () => ({ useSetLocalStorage: () => setEducationVerifyingMock, })) diff --git a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts index 289af673712..6162869e1e7 100644 --- a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts +++ b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts @@ -3,12 +3,12 @@ import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-moda import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' import type { EnvironmentVariable } from '@/app/components/workflow/types' import { toast } from '@langgenius/dify-ui/toast' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useProviderContext } from '@/context/provider-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useRouter } from '@/next/navigation' import { copyApp, deleteApp, exportAppConfig, fetchAppDetail, updateAppInfo } from '@/service/apps' import { useInvalidateAppList } from '@/service/use-apps' diff --git a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx index ccc2bd4ead3..dd7fd42dc8d 100644 --- a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx +++ b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx @@ -19,6 +19,7 @@ import { useBoolean, useSessionStorageState, } from 'ahooks' +import { useLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -27,7 +28,6 @@ import Loading from '@/app/components/base/loading' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' -import { useLocalStorage } from '@/hooks/use-local-storage' import { generateRule } from '@/service/debug' import { useGenerateRuleTemplate } from '@/service/use-apps' import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index' diff --git a/web/app/components/app/create-app-dialog/app-list/index.tsx b/web/app/components/app/create-app-dialog/app-list/index.tsx index 51ce7c076fc..ba0ba8e1b49 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.tsx @@ -6,6 +6,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' import { RiRobot2Line } from '@remixicon/react' import { useDebounceFn } from 'ahooks' +import { useSetLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -17,7 +18,6 @@ import CreateAppModal from '@/app/components/explore/create-app-modal' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { DSLImportMode } from '@/models/app' import { useRouter } from '@/next/navigation' import { importDSL } from '@/service/apps' diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index 0d29c4d5992..0628e66e166 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -10,6 +10,7 @@ import { toast } from '@langgenius/dify-ui/toast' import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon/react' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' import { useDebounceFn } from 'ahooks' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' @@ -20,7 +21,6 @@ import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import useTheme from '@/hooks/use-theme' import { useRouter } from '@/next/navigation' import { createApp } from '@/service/apps' diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index 72eac7ef797..bbe4e114d01 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -8,6 +8,7 @@ import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { toast } from '@langgenius/dify-ui/toast' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' import { useDebounceFn } from 'ahooks' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' @@ -16,7 +17,6 @@ import { usePluginDependencies } from '@/app/components/workflow/plugin-dependen import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { DSLImportMode, DSLImportStatus, diff --git a/web/app/components/app/switch-app-modal/index.tsx b/web/app/components/app/switch-app-modal/index.tsx index f94f6735bff..0c794025062 100644 --- a/web/app/components/app/switch-app-modal/index.tsx +++ b/web/app/components/app/switch-app-modal/index.tsx @@ -16,6 +16,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' @@ -26,7 +27,6 @@ import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useRouter } from '@/next/navigation' import { deleteApp, switchApp } from '@/service/apps' import { AppModeEnum } from '@/types/app' diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index 7fe19ac4e13..c6e5ecb7330 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -30,6 +30,7 @@ import { TooltipTrigger, } from '@langgenius/dify-ui/tooltip' import { useSuspenseQuery } from '@tanstack/react-query' +import { useSetLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useCallback, useMemo, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' @@ -42,7 +43,6 @@ import { useProviderContext } from '@/context/provider-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { AppCardTags } from '@/features/tag-management/components/app-card-tags' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { AccessMode } from '@/models/access-control' import dynamic from '@/next/dynamic' import { useRouter } from '@/next/navigation' diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index 321c104462e..84be0873339 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -4,6 +4,7 @@ import type { AppListQuery } from '@/contract/console/apps' import { cn } from '@langgenius/dify-ui/cn' import { keepPreviousData, useInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query' import { useDebounce } from 'ahooks' +import { useLocalStorage } from 'foxact/use-local-storage' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { SearchInput } from '@/app/components/base/search-input' @@ -11,7 +12,6 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { TagFilter } from '@/features/tag-management/components/tag-filter' -import { useLocalStorage } from '@/hooks/use-local-storage' import { CheckModal } from '@/hooks/use-pay' import dynamic from '@/next/dynamic' import Link from '@/next/link' diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 765a32ba69d..c2936a71ee5 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -5,6 +5,7 @@ import type { AppData, ConversationItem } from '@/models/share' import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow' import { toast } from '@langgenius/dify-ui/toast' import { noop } from 'es-toolkit/function' +import { useLocalStorage } from 'foxact/use-local-storage' import { produce } from 'immer' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -12,7 +13,6 @@ import { getProcessedFilesFromResponse } from '@/app/components/base/file-upload import { InputVarType } from '@/app/components/workflow/types' import { useWebAppStore } from '@/context/web-app-context' import { useAppFavicon } from '@/hooks/use-app-favicon' -import { useLocalStorage } from '@/hooks/use-local-storage' import { changeLanguage } from '@/i18n-config/client' import { AppSourceType, delConversation, pinConversation, renameConversation, unpinConversation, updateFeedback } from '@/service/share' import { useInvalidateShareConversations, useShareChatList, useShareConversationName, useShareConversations } from '@/service/use-share' diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 948be4ae3f4..b9c2df19b3d 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -5,13 +5,13 @@ import type { Locale } from '@/i18n-config' import type { AppData, ConversationItem } from '@/models/share' import { toast } from '@langgenius/dify-ui/toast' import { noop } from 'es-toolkit/function' +import { useLocalStorage } from 'foxact/use-local-storage' import { produce } from 'immer' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { InputVarType } from '@/app/components/workflow/types' import { useWebAppStore } from '@/context/web-app-context' -import { useLocalStorage } from '@/hooks/use-local-storage' import { changeLanguage } from '@/i18n-config/client' import { AppSourceType, updateFeedback } from '@/service/share' import { useInvalidateShareConversations, useShareChatList, useShareConversationName, useShareConversations } from '@/service/use-share' diff --git a/web/app/components/base/copy-feedback/__tests__/index.spec.tsx b/web/app/components/base/copy-feedback/__tests__/index.spec.tsx index b2484031dbf..f92925ae89d 100644 --- a/web/app/components/base/copy-feedback/__tests__/index.spec.tsx +++ b/web/app/components/base/copy-feedback/__tests__/index.spec.tsx @@ -5,7 +5,7 @@ const mockCopy = vi.fn() const mockReset = vi.fn() let mockCopied = false -vi.mock('@/hooks/use-clipboard', () => ({ +vi.mock('foxact/use-clipboard', () => ({ useClipboard: () => ({ copy: mockCopy, reset: mockReset, diff --git a/web/app/components/base/copy-feedback/index.tsx b/web/app/components/base/copy-feedback/index.tsx index 12b70b3c0e2..8415d0c433e 100644 --- a/web/app/components/base/copy-feedback/index.tsx +++ b/web/app/components/base/copy-feedback/index.tsx @@ -4,10 +4,10 @@ import { RiClipboardFill, RiClipboardLine, } from '@remixicon/react' +import { useClipboard } from 'foxact/use-clipboard' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' -import { useClipboard } from '@/hooks/use-clipboard' import copyStyle from './style.module.css' type Props = Readonly<{ diff --git a/web/app/components/base/copy-icon/__tests__/index.spec.tsx b/web/app/components/base/copy-icon/__tests__/index.spec.tsx index 28044a20d03..568d6c92843 100644 --- a/web/app/components/base/copy-icon/__tests__/index.spec.tsx +++ b/web/app/components/base/copy-icon/__tests__/index.spec.tsx @@ -5,7 +5,7 @@ const copy = vi.fn() const reset = vi.fn() let copied = false -vi.mock('@/hooks/use-clipboard', () => ({ +vi.mock('foxact/use-clipboard', () => ({ useClipboard: () => ({ copy, reset, diff --git a/web/app/components/base/copy-icon/index.tsx b/web/app/components/base/copy-icon/index.tsx index 6c6fa987a0e..5ed40bc1fdd 100644 --- a/web/app/components/base/copy-icon/index.tsx +++ b/web/app/components/base/copy-icon/index.tsx @@ -1,8 +1,8 @@ 'use client' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { useClipboard } from 'foxact/use-clipboard' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { useClipboard } from '@/hooks/use-clipboard' type Props = Readonly<{ content: string diff --git a/web/app/components/base/input-with-copy/__tests__/index.spec.tsx b/web/app/components/base/input-with-copy/__tests__/index.spec.tsx index b985a0f018f..2b9d64a39ac 100644 --- a/web/app/components/base/input-with-copy/__tests__/index.spec.tsx +++ b/web/app/components/base/input-with-copy/__tests__/index.spec.tsx @@ -6,7 +6,7 @@ const mockCopy = vi.fn() let mockCopied = false const mockReset = vi.fn() -vi.mock('@/hooks/use-clipboard', () => ({ +vi.mock('foxact/use-clipboard', () => ({ useClipboard: () => ({ copy: mockCopy, copied: mockCopied, diff --git a/web/app/components/base/input-with-copy/index.tsx b/web/app/components/base/input-with-copy/index.tsx index e401b7bdb08..6f6682bcee3 100644 --- a/web/app/components/base/input-with-copy/index.tsx +++ b/web/app/components/base/input-with-copy/index.tsx @@ -2,9 +2,9 @@ import type { InputProps } from '../input' import { cn } from '@langgenius/dify-ui/cn' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { useClipboard } from 'foxact/use-clipboard' import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useClipboard } from '@/hooks/use-clipboard' import ActionButton from '../action-button' type InputWithCopyProps = { diff --git a/web/app/components/billing/plan/__tests__/index.spec.tsx b/web/app/components/billing/plan/__tests__/index.spec.tsx index b433d5df274..5038f57f882 100644 --- a/web/app/components/billing/plan/__tests__/index.spec.tsx +++ b/web/app/components/billing/plan/__tests__/index.spec.tsx @@ -36,7 +36,7 @@ vi.mock('@/context/app-context', () => ({ }), })) -vi.mock('@/hooks/use-local-storage', () => ({ +vi.mock('foxact/use-local-storage', () => ({ useSetLocalStorage: () => setEducationVerifyingMock, })) diff --git a/web/app/components/billing/plan/index.tsx b/web/app/components/billing/plan/index.tsx index 55d4a714d2e..e76925abbf9 100644 --- a/web/app/components/billing/plan/index.tsx +++ b/web/app/components/billing/plan/index.tsx @@ -8,6 +8,7 @@ import { RiGroupLine, } from '@remixicon/react' import { useUnmountedRef } from 'ahooks' +import { useSetLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useEffect } from 'react' import { useTranslation } from 'react-i18next' @@ -18,7 +19,6 @@ import VerifyStateModal from '@/app/education-apply/verify-state-modal' import { useAppContext } from '@/context/app-context' import { useModalContextSelector } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { usePathname, useRouter } from '@/next/navigation' import { useEducationVerify } from '@/service/use-education' import { getDaysUntilEndOfMonth } from '@/utils/time' diff --git a/web/app/components/education-verify-action-recorder.tsx b/web/app/components/education-verify-action-recorder.tsx index 3e26e55dc77..ece0569c7e2 100644 --- a/web/app/components/education-verify-action-recorder.tsx +++ b/web/app/components/education-verify-action-recorder.tsx @@ -1,11 +1,11 @@ 'use client' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useEffect } from 'react' import { EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION, EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, } from '@/app/education-apply/constants' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useSearchParams } from '@/next/navigation' export function EducationVerifyActionRecorder() { diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index 896f7416ce4..0c2148ed884 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -5,6 +5,7 @@ import { Avatar } from '@langgenius/dify-ui/avatar' import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { useSuspenseQuery } from '@tanstack/react-query' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { resetUser } from '@/app/components/base/amplitude/utils' @@ -18,7 +19,6 @@ import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { env } from '@/env' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import Link from '@/next/link' import { useRouter } from '@/next/navigation' import { useLogout } from '@/service/use-common' diff --git a/web/app/components/header/header-wrapper.tsx b/web/app/components/header/header-wrapper.tsx index 4c5c8e657e9..5f35a95524a 100644 --- a/web/app/components/header/header-wrapper.tsx +++ b/web/app/components/header/header-wrapper.tsx @@ -1,10 +1,10 @@ 'use client' import type { EventEmitterValue } from '@/context/event-emitter' import { cn } from '@langgenius/dify-ui/cn' +import { useLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useState } from 'react' import { useEventEmitterContextContext } from '@/context/event-emitter' -import { useLocalStorage } from '@/hooks/use-local-storage' import { usePathname } from '@/next/navigation' import s from './index.module.css' diff --git a/web/app/components/header/maintenance-notice.tsx b/web/app/components/header/maintenance-notice.tsx index 559402aa12f..d3a6481f1a5 100644 --- a/web/app/components/header/maintenance-notice.tsx +++ b/web/app/components/header/maintenance-notice.tsx @@ -1,8 +1,8 @@ +import { useLocalStorage } from 'foxact/use-local-storage' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { X } from '@/app/components/base/icons/src/vender/line/general' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' -import { useLocalStorage } from '@/hooks/use-local-storage' import { NOTICE_I18N } from '@/i18n-config/language' const MaintenanceNotice = () => { diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index 82a8637cd20..8a0463e915f 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -6,6 +6,7 @@ import type { Plugin } from '@/app/components/plugins/types' import type { Locale } from '@/i18n-config' import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import { RiMoreLine } from '@remixicon/react' +import { useLocalStorage } from 'foxact/use-local-storage' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { ArrowDownDoubleLine, ArrowDownRoundFill, ArrowUpDoubleLine } from '@/app/components/base/icons/src/vender/solid/arrows' @@ -13,7 +14,6 @@ import Loading from '@/app/components/base/loading' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import { useGetLanguage } from '@/context/i18n' -import { useLocalStorage } from '@/hooks/use-local-storage' import Link from '@/next/link' import { formatNumber } from '@/utils/format' import { getMarketplaceUrl } from '@/utils/var' diff --git a/web/app/components/workflow/block-selector/featured-triggers.tsx b/web/app/components/workflow/block-selector/featured-triggers.tsx index 8dec59d8cc8..13efb38c8f2 100644 --- a/web/app/components/workflow/block-selector/featured-triggers.tsx +++ b/web/app/components/workflow/block-selector/featured-triggers.tsx @@ -6,6 +6,7 @@ import type { Plugin } from '@/app/components/plugins/types' import type { Locale } from '@/i18n-config' import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import { RiMoreLine } from '@remixicon/react' +import { useLocalStorage } from 'foxact/use-local-storage' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { ArrowDownDoubleLine, ArrowDownRoundFill, ArrowUpDoubleLine } from '@/app/components/base/icons/src/vender/solid/arrows' @@ -13,7 +14,6 @@ import Loading from '@/app/components/base/loading' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import { useGetLanguage } from '@/context/i18n' -import { useLocalStorage } from '@/hooks/use-local-storage' import Link from '@/next/link' import { formatNumber } from '@/utils/format' import { getMarketplaceUrl } from '@/utils/var' diff --git a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx index 2bf0b51dba9..f7def41acd5 100644 --- a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx +++ b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx @@ -3,13 +3,13 @@ import type { Dispatch, SetStateAction } from 'react' import type { ViewType } from '@/app/components/workflow/block-selector/view-type-select' import type { OnSelectBlock } from '@/app/components/workflow/types' import { RiMoreLine } from '@remixicon/react' +import { useLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useCallback, useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/arrows' import Loading from '@/app/components/base/loading' import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils' -import { useLocalStorage } from '@/hooks/use-local-storage' import Link from '@/next/link' import { useRAGRecommendedPlugins } from '@/service/use-tools' import { getMarketplaceUrl } from '@/utils/var' diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 6abec226ce2..c73361e35c3 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -13,6 +13,7 @@ import { RiPlayLargeLine, } from '@remixicon/react' import { debounce } from 'es-toolkit/compat' +import { useSetLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { cloneElement, @@ -68,7 +69,6 @@ import { } from '@/app/components/workflow/utils' import { useAppContext } from '@/context/app-context' import { useModalContext } from '@/context/modal-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useAllBuiltInTools } from '@/service/use-tools' import { useAllTriggerPlugins } from '@/service/use-triggers' import { FlowType } from '@/types/common' diff --git a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx index e8a809b5606..672536aabed 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx @@ -5,13 +5,13 @@ import type { ValueSelector, Var } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' import { RiDraggable } from '@remixicon/react' import { noop } from 'es-toolkit/function' +import { useLocalStorage } from 'foxact/use-local-storage' import { produce } from 'immer' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { ReactSortable } from 'react-sortablejs' import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general' -import { useLocalStorage } from '@/hooks/use-local-storage' import { useEdgesInteractions } from '../../../hooks' import AddButton from '../../_base/components/add-button' import Item from './class-item' diff --git a/web/app/components/workflow/note-node/hooks.ts b/web/app/components/workflow/note-node/hooks.ts index 82d13e09bca..8a9e3573eba 100644 --- a/web/app/components/workflow/note-node/hooks.ts +++ b/web/app/components/workflow/note-node/hooks.ts @@ -1,7 +1,7 @@ import type { EditorState } from 'lexical' import type { NoteTheme } from './types' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback } from 'react' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useNodeDataUpdate, useWorkflowHistory, WorkflowHistoryEvent } from '../hooks' import { NOTE_SHOW_AUTHOR_STORAGE_KEY } from './constants' diff --git a/web/app/components/workflow/operator/hooks.ts b/web/app/components/workflow/operator/hooks.ts index adf2befd800..4330920d501 100644 --- a/web/app/components/workflow/operator/hooks.ts +++ b/web/app/components/workflow/operator/hooks.ts @@ -1,7 +1,7 @@ import type { NoteNodeType } from '../note-node/types' +import { useLocalStorage } from 'foxact/use-local-storage' import { useCallback } from 'react' import { useAppContext } from '@/context/app-context' -import { useLocalStorage } from '@/hooks/use-local-storage' import { CUSTOM_NOTE_NODE, NOTE_SHOW_AUTHOR_STORAGE_KEY, diff --git a/web/app/components/workflow/panel/debug-and-preview/index.tsx b/web/app/components/workflow/panel/debug-and-preview/index.tsx index 6e246df9e11..3f0576d8703 100644 --- a/web/app/components/workflow/panel/debug-and-preview/index.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/index.tsx @@ -5,6 +5,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/too import { RiCloseLine, RiEqualizer2Line } from '@remixicon/react' import { debounce } from 'es-toolkit/compat' import { noop } from 'es-toolkit/function' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { memo, useCallback, @@ -19,7 +20,6 @@ import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync' import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' import { useStore } from '@/app/components/workflow/store' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useWorkflowInteractions, } from '../../hooks' diff --git a/web/app/components/workflow/persistence/local-storage-bridge.tsx b/web/app/components/workflow/persistence/local-storage-bridge.tsx index 07eac23396d..437917162e1 100644 --- a/web/app/components/workflow/persistence/local-storage-bridge.tsx +++ b/web/app/components/workflow/persistence/local-storage-bridge.tsx @@ -1,5 +1,5 @@ +import { useLocalStorage, useSetLocalStorage } from 'foxact/use-local-storage' import { useEffect, useLayoutEffect as useLayoutEffectFromReact } from 'react' -import { useLocalStorage, useSetLocalStorage } from '@/hooks/use-local-storage' import { useStore, useWorkflowStore } from '../store' import { isControlMode, diff --git a/web/app/components/workflow/variable-inspect/index.tsx b/web/app/components/workflow/variable-inspect/index.tsx index 828c992d9d7..924ad7d07c0 100644 --- a/web/app/components/workflow/variable-inspect/index.tsx +++ b/web/app/components/workflow/variable-inspect/index.tsx @@ -1,11 +1,11 @@ import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' import { debounce } from 'es-toolkit/compat' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback, useMemo, } from 'react' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useResizePanel } from '../nodes/_base/hooks/use-resize-panel' import { useStore } from '../store' import Panel from './panel' diff --git a/web/app/components/workflow/workflow-generator/index.tsx b/web/app/components/workflow/workflow-generator/index.tsx index b7c2bd95f3c..cd0128c2797 100644 --- a/web/app/components/workflow/workflow-generator/index.tsx +++ b/web/app/components/workflow/workflow-generator/index.tsx @@ -16,6 +16,7 @@ import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Textarea } from '@langgenius/dify-ui/textarea' import { toast } from '@langgenius/dify-ui/toast' import { useBoolean } from 'ahooks' +import { useLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -26,7 +27,6 @@ import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/com import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import WorkflowPreview from '@/app/components/workflow/workflow-preview' import { useAppContext } from '@/context/app-context' -import { useLocalStorage } from '@/hooks/use-local-storage' import { useRouter } from '@/next/navigation' import { generateWorkflow } from '@/service/debug' import { fetchWorkflowDraft } from '@/service/workflow' diff --git a/web/app/education-apply/education-apply-page.tsx b/web/app/education-apply/education-apply-page.tsx index 4e5aedc68c5..4c5c310fac3 100644 --- a/web/app/education-apply/education-apply-page.tsx +++ b/web/app/education-apply/education-apply-page.tsx @@ -8,6 +8,7 @@ import { Checkbox } from '@langgenius/dify-ui/checkbox' import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import { noop } from 'es-toolkit/function' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { useEducationDiscount } from '@/app/components/billing/hooks/use-education-discount' @@ -19,7 +20,6 @@ import { useProviderContext } from '@/context/provider-context' import { useWorkspacesContext } from '@/context/workspace-context' import { WorkspaceProvider } from '@/context/workspace-context-provider' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useRouter, useSearchParams, diff --git a/web/app/education-apply/hooks.ts b/web/app/education-apply/hooks.ts index 764ab1a0904..ae216feebc2 100644 --- a/web/app/education-apply/hooks.ts +++ b/web/app/education-apply/hooks.ts @@ -4,6 +4,7 @@ import { useDebounceFn } from 'ahooks' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' +import { useLocalStorage } from 'foxact/use-local-storage' import { useCallback, useEffect, @@ -13,7 +14,6 @@ import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/con import { useModalContextSelector } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { userProfileQueryOptions } from '@/features/account-profile/client' -import { useLocalStorage } from '@/hooks/use-local-storage' import { useRouter, useSearchParams } from '@/next/navigation' import { useEducationAutocomplete, useEducationVerify } from '@/service/use-education' import { diff --git a/web/app/signin/components/mail-and-code-auth.tsx b/web/app/signin/components/mail-and-code-auth.tsx index eacf1e8acd2..2c013b868bc 100644 --- a/web/app/signin/components/mail-and-code-auth.tsx +++ b/web/app/signin/components/mail-and-code-auth.tsx @@ -2,12 +2,12 @@ import { Button } from '@langgenius/dify-ui/button' import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { toast } from '@langgenius/dify-ui/toast' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown' import { emailRegex } from '@/config' import { useLocale } from '@/context/i18n' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useRouter, useSearchParams } from '@/next/navigation' import { sendEMailLoginCode } from '@/service/common' diff --git a/web/context/modal-context-provider.tsx b/web/context/modal-context-provider.tsx index 4adc1c25d28..0f6392115c3 100644 --- a/web/context/modal-context-provider.tsx +++ b/web/context/modal-context-provider.tsx @@ -11,6 +11,7 @@ import type { InputVar } from '@/app/components/workflow/types' import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal' import type { ExternalDataTool } from '@/models/common' import type { ModerationConfig, PromptVariable } from '@/models/debug' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback, useEffect, useRef, useState } from 'react' import { @@ -22,7 +23,6 @@ import { } from '@/app/education-apply/constants' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useAccountSettingModal, usePricingModal, diff --git a/web/context/modal-context.test.tsx b/web/context/modal-context.test.tsx index 62ce95ef978..d31c3f1a6ac 100644 --- a/web/context/modal-context.test.tsx +++ b/web/context/modal-context.test.tsx @@ -32,7 +32,7 @@ vi.mock('@/app/components/header/account-setting', () => ({ ), })) -vi.mock('@/hooks/use-local-storage', () => ({ +vi.mock('foxact/use-local-storage', () => ({ useSetLocalStorage: () => mockSetEducationVerifying, })) diff --git a/web/context/provider-context-provider.tsx b/web/context/provider-context-provider.tsx index 90a47451bfb..e95570bae1f 100644 --- a/web/context/provider-context-provider.tsx +++ b/web/context/provider-context-provider.tsx @@ -4,6 +4,7 @@ import type { ReactNode } from 'react' import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' +import { useLocalStorage } from 'foxact/use-local-storage' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils' @@ -15,7 +16,6 @@ import { ModelTypeEnum, } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ZENDESK_FIELD_IDS } from '@/config' -import { useLocalStorage } from '@/hooks/use-local-storage' import { fetchCurrentPlanInfo } from '@/service/billing' import { useModelListByType, diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 3e5b6213687..7c096cc84f9 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -186,14 +186,13 @@ export default antfu( ...GLOB_TESTS, 'vitest.setup.ts', 'instrumentation-client.ts', - 'hooks/use-local-storage/index.ts', ], rules: { 'no-restricted-globals': [ 'error', { name: 'localStorage', - message: 'Do not use localStorage directly. Use @/hooks/use-local-storage instead.', + message: 'Do not use localStorage directly. Use foxact/use-local-storage instead.', }, ], 'no-restricted-properties': [ @@ -201,19 +200,19 @@ export default antfu( { object: 'window', property: 'localStorage', - message: 'Do not use window.localStorage directly. Use @/hooks/use-local-storage instead.', + message: 'Do not use window.localStorage directly. Use foxact/use-local-storage instead.', }, { object: 'globalThis', property: 'localStorage', - message: 'Do not use globalThis.localStorage directly. Use @/hooks/use-local-storage instead.', + message: 'Do not use globalThis.localStorage directly. Use foxact/use-local-storage instead.', }, ], 'no-restricted-syntax': [ 'error', { selector: 'ImportDeclaration[source.value="ahooks"] ImportSpecifier[imported.name="useLocalStorageState"]', - message: 'Do not use ahooks useLocalStorageState. Use @/hooks/use-local-storage instead.', + message: 'Do not use ahooks useLocalStorageState. Use foxact/use-local-storage instead.', }, ], }, diff --git a/web/hooks/noop.ts b/web/hooks/noop.ts deleted file mode 100644 index 9cf6f968dcb..00000000000 --- a/web/hooks/noop.ts +++ /dev/null @@ -1,7 +0,0 @@ -type Noop = { - // eslint-disable-next-line ts/no-explicit-any - (...args: any[]): any -} - -/** @see https://foxact.skk.moe/noop */ -export const noop: Noop = () => { /* noop */ } diff --git a/web/hooks/use-clipboard.ts b/web/hooks/use-clipboard.ts deleted file mode 100644 index 60ceeb4f188..00000000000 --- a/web/hooks/use-clipboard.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { useRef, useState } from 'react' -import { writeTextToClipboard } from '@/utils/clipboard' -import { noop } from './noop' -import { useStableHandler } from './use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired' -import { useCallback } from './use-typescript-happy-callback' -import 'client-only' - -type UseClipboardOption = { - timeout?: number - usePromptAsFallback?: boolean - promptFallbackText?: string - onCopyError?: (error: Error) => void -} - -/** @see https://foxact.skk.moe/use-clipboard */ -export function useClipboard({ - timeout = 1000, - usePromptAsFallback = false, - promptFallbackText = 'Failed to copy to clipboard automatically, please manually copy the text below.', - onCopyError, -}: UseClipboardOption = {}) { - const [error, setError] = useState(null) - const [copied, setCopied] = useState(false) - const copyTimeoutRef = useRef(null) - - const stablizedOnCopyError = useStableHandler<[e: Error], void>(onCopyError || noop) - - const handleCopyResult = useCallback((isCopied: boolean) => { - if (copyTimeoutRef.current) { - clearTimeout(copyTimeoutRef.current) - } - if (isCopied) { - copyTimeoutRef.current = window.setTimeout(() => setCopied(false), timeout) - } - setCopied(isCopied) - }, [timeout]) - - const handleCopyError = useCallback((e: Error) => { - setError(e) - stablizedOnCopyError(e) - }, [stablizedOnCopyError]) - - const copy = useCallback(async (valueToCopy: string) => { - try { - await writeTextToClipboard(valueToCopy) - handleCopyResult(true) - } - catch (e) { - if (usePromptAsFallback) { - try { - // eslint-disable-next-line no-alert -- prompt as fallback in case of copy error - window.prompt(promptFallbackText, valueToCopy) - handleCopyResult(true) - } - catch (e2) { - handleCopyError(e2 as Error) - } - } - else { - handleCopyError(e as Error) - } - } - }, [handleCopyResult, promptFallbackText, handleCopyError, usePromptAsFallback]) - - const reset = useCallback(() => { - setCopied(false) - setError(null) - if (copyTimeoutRef.current) { - clearTimeout(copyTimeoutRef.current) - } - }, []) - - return { copy, reset, error, copied } -} diff --git a/web/hooks/use-import-dsl.ts b/web/hooks/use-import-dsl.ts index bfcb8dc1943..6fe157bcfa5 100644 --- a/web/hooks/use-import-dsl.ts +++ b/web/hooks/use-import-dsl.ts @@ -4,6 +4,7 @@ import type { } from '@/models/app' import type { AppIconType } from '@/types/app' import { toast } from '@langgenius/dify-ui/toast' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback, useRef, @@ -13,7 +14,6 @@ import { useTranslation } from 'react-i18next' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useSelector } from '@/context/app-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { DSLImportStatus } from '@/models/app' import { useRouter } from '@/next/navigation' import { diff --git a/web/hooks/use-local-storage/__tests__/index.spec.tsx b/web/hooks/use-local-storage/__tests__/index.spec.tsx deleted file mode 100644 index 1819f46f262..00000000000 --- a/web/hooks/use-local-storage/__tests__/index.spec.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { act, renderHook } from '@testing-library/react' -import { renderToString } from 'react-dom/server' -import { useLocalStorage } from '../index' - -describe('useLocalStorage', () => { - beforeEach(() => { - vi.clearAllMocks() - window.localStorage.clear() - }) - - it('should return server value and persist it when storage is empty', () => { - const { result } = renderHook(() => useLocalStorage('shape', 'circle')) - - expect(result.current[0]).toBe('circle') - expect(window.localStorage.getItem('shape')).toBe(JSON.stringify('circle')) - }) - - it('should prefer stored value over server value', () => { - window.localStorage.setItem('shape', JSON.stringify('square')) - - const { result } = renderHook(() => useLocalStorage('shape', 'circle')) - - expect(result.current[0]).toBe('square') - expect(window.localStorage.getItem('shape')).toBe(JSON.stringify('square')) - }) - - it('should update storage and subscribers from setter calls', () => { - const { result } = renderHook(() => useLocalStorage('shape', 'circle')) - - act(() => { - result.current[1]('triangle') - }) - - expect(result.current[0]).toBe('triangle') - expect(window.localStorage.getItem('shape')).toBe(JSON.stringify('triangle')) - }) - - it('should support updater functions and null removal', () => { - const { result } = renderHook(() => useLocalStorage('count', 1)) - - act(() => { - result.current[1](current => (current ?? 0) + 1) - }) - - expect(result.current[0]).toBe(2) - expect(window.localStorage.getItem('count')).toBe(JSON.stringify(2)) - - act(() => { - result.current[1](null) - }) - - expect(result.current[0]).toBe(1) - expect(window.localStorage.getItem('count')).toBeNull() - }) - - it('should update from cross-tab storage events', () => { - const { result } = renderHook(() => useLocalStorage('shape', 'circle')) - - window.localStorage.setItem('shape', JSON.stringify('square')) - act(() => { - window.dispatchEvent(new StorageEvent('storage', { key: 'shape' })) - }) - - expect(result.current[0]).toBe('square') - }) - - it('should support raw string values', () => { - const { result } = renderHook(() => useLocalStorage('raw-shape', 'circle', { raw: true })) - - act(() => { - result.current[1]('square') - }) - - expect(result.current[0]).toBe('square') - expect(window.localStorage.getItem('raw-shape')).toBe('square') - }) - - it('should render with server value during server rendering', () => { - const Component = () => { - const [value] = useLocalStorage('shape', 'circle') - return
{value}
- } - - expect(renderToString()).toContain('circle') - }) - - it('should throw a recoverable no-SSR error during server rendering without server value', () => { - const Component = () => { - const [value] = useLocalStorage('shape') - return
{value}
- } - - expect(() => renderToString()).toThrow('[foxact/use-local-storage] cannot be used on the server without a serverValue') - }) -}) diff --git a/web/hooks/use-local-storage/index.ts b/web/hooks/use-local-storage/index.ts deleted file mode 100644 index da98e5feb4f..00000000000 --- a/web/hooks/use-local-storage/index.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { useCallback, useEffect, useLayoutEffect as useLayoutEffectFromReact, useMemo, useSyncExternalStore } from 'react' -import { noop } from '../noop' -import 'client-only' - -type NotUndefined = T extends undefined ? never : T -type StateHookTuple = readonly [T, React.Dispatch>] - -type Serializer = (value: T) => string -type Deserializer = (value: string) => T - -export type UseLocalStorageRawOption = { - raw: true -} - -export type UseLocalStorageParserOption = { - raw?: false - serializer: Serializer - deserializer: Deserializer -} - -const FOXACT_LOCAL_STORAGE_EVENT_KEY = 'foxact-use-local-storage' -const HOOK_NAME = 'foxact/use-local-storage' - -const useLayoutEffect = typeof window === 'undefined' - ? useEffect - : useLayoutEffectFromReact - -type ErrorConstructorWithStackTraceLimit = ErrorConstructor & { - stackTraceLimit?: number -} - -const errorConstructor = Error as ErrorConstructorWithStackTraceLimit -const stackTraceLimitProperty = Object.getOwnPropertyDescriptor(errorConstructor, 'stackTraceLimit') -const hasWritableStackTraceLimit = stackTraceLimitProperty?.writable && typeof stackTraceLimitProperty.value === 'number' - -function createStacklessError(errorFactory: () => T): T { - const originalStackTraceLimit = errorConstructor.stackTraceLimit - - if (hasWritableStackTraceLimit) - errorConstructor.stackTraceLimit = 0 - - const error = errorFactory() - - if (hasWritableStackTraceLimit) - errorConstructor.stackTraceLimit = originalStackTraceLimit - - return error -} - -function noSSRError(errorMessage?: string, nextjsDigest = 'BAILOUT_TO_CLIENT_SIDE_RENDERING') { - const error = createStacklessError(() => new Error(errorMessage)) as Error & { - digest?: string - recoverableError?: string - } - - error.digest = nextjsDigest - error.recoverableError = 'NO_SSR' - - return error -} - -function getServerSnapshotWithoutServerValue(): never { - throw noSSRError(`[${HOOK_NAME}] cannot be used on the server without a serverValue`) -} - -function rawSerializer(value: T): string { - return value as string -} - -function rawDeserializer(value: string): T { - return value as T -} - -function isStorageSetter( - value: React.SetStateAction, -): value is (previousState: T | null) => T | null { - return typeof value === 'function' -} - -const dispatchStorageEvent = typeof window === 'undefined' - ? noop - : (key: string) => { - window.dispatchEvent(new CustomEvent(FOXACT_LOCAL_STORAGE_EVENT_KEY, { detail: key })) - } - -const setStorageItem = typeof window === 'undefined' - ? noop - : (key: string, value: string) => { - try { - window.localStorage.setItem(key, value) - } - catch { - console.warn(`[${HOOK_NAME}] Failed to set value to localStorage, it might be blocked`) - } - finally { - dispatchStorageEvent(key) - } - } - -const removeStorageItem = typeof window === 'undefined' - ? noop - : (key: string) => { - try { - window.localStorage.removeItem(key) - } - catch { - console.warn(`[${HOOK_NAME}] Failed to remove value from localStorage, it might be blocked`) - } - finally { - dispatchStorageEvent(key) - } - } - -function getStorageItem(key: string) { - if (typeof window === 'undefined') - return null - - try { - return window.localStorage.getItem(key) - } - catch { - console.warn(`[${HOOK_NAME}] Failed to get value from localStorage, it might be blocked`) - return null - } -} - -function getStorageOptions( - options: UseLocalStorageRawOption | UseLocalStorageParserOption, -) { - return options.raw - ? { - serializer: rawSerializer, - deserializer: rawDeserializer, - } - : { - serializer: options.serializer, - deserializer: options.deserializer, - } -} - -const defaultStorageOptions = { - raw: false, - serializer: JSON.stringify, - deserializer: JSON.parse, -} satisfies UseLocalStorageParserOption - -/** @see https://foxact.skk.moe/use-local-storage */ -export const useSetLocalStorage = ( - key: string, - options: UseLocalStorageRawOption | UseLocalStorageParserOption = defaultStorageOptions as UseLocalStorageParserOption, -) => { - const { serializer, deserializer } = getStorageOptions(options) - - return useCallback((value: React.SetStateAction) => { - try { - let nextState: T | null - if (isStorageSetter(value)) { - const currentRaw = getStorageItem(key) - const currentState = currentRaw === null ? null : deserializer(currentRaw) - nextState = value(currentState) - } - else { - nextState = value - } - - if (nextState === null) - removeStorageItem(key) - else - setStorageItem(key, serializer(nextState)) - } - catch (error) { - console.warn(error) - } - }, [key, serializer, deserializer]) -} - -function useLocalStorageValue( - key: string, - serverValue: NotUndefined, - options?: UseLocalStorageRawOption | UseLocalStorageParserOption, -): T -function useLocalStorageValue( - key: string, - serverValue?: undefined, - options?: UseLocalStorageRawOption | UseLocalStorageParserOption, -): T | null -function useLocalStorageValue( - key: string, - serverValue?: NotUndefined, - options: UseLocalStorageRawOption | UseLocalStorageParserOption = defaultStorageOptions as UseLocalStorageParserOption, -): T | null { - const subscribeToSpecificKeyOfLocalStorage = useCallback((callback: () => void) => { - if (typeof window === 'undefined') - return noop - - const handleStorageEvent = (event: StorageEvent) => { - if (!('key' in event) || event.key === key) - callback() - } - const handleCustomStorageEvent: EventListener = (event) => { - if (event instanceof CustomEvent && event.detail === key) - callback() - } - - window.addEventListener('storage', handleStorageEvent) - window.addEventListener(FOXACT_LOCAL_STORAGE_EVENT_KEY, handleCustomStorageEvent) - - return () => { - window.removeEventListener('storage', handleStorageEvent) - window.removeEventListener(FOXACT_LOCAL_STORAGE_EVENT_KEY, handleCustomStorageEvent) - } - }, [key]) - - const { serializer, deserializer } = getStorageOptions(options) - const getClientSnapshot = () => getStorageItem(key) - const getServerSnapshot = serverValue === undefined - ? getServerSnapshotWithoutServerValue - : () => serializer(serverValue) - - const store = useSyncExternalStore( - subscribeToSpecificKeyOfLocalStorage, - getClientSnapshot, - getServerSnapshot, - ) - - const deserialized = useMemo(() => (store === null ? null : deserializer(store)), [store, deserializer]) - - useLayoutEffect(() => { - if (getStorageItem(key) === null && serverValue !== undefined) - setStorageItem(key, serializer(serverValue)) - }, [key, serializer, serverValue]) - - return deserialized === null - ? (serverValue === undefined ? null : serverValue) - : deserialized -} - -function useLocalStorage( - key: string, - serverValue: NotUndefined, - options?: UseLocalStorageRawOption | UseLocalStorageParserOption, -): StateHookTuple -function useLocalStorage( - key: string, - serverValue?: undefined, - options?: UseLocalStorageRawOption | UseLocalStorageParserOption, -): StateHookTuple -/** @see https://foxact.skk.moe/use-local-storage */ -function useLocalStorage( - key: string, - serverValue?: NotUndefined, - options: UseLocalStorageRawOption | UseLocalStorageParserOption = defaultStorageOptions as UseLocalStorageParserOption, -): StateHookTuple | StateHookTuple { - const value = useLocalStorageValue(key, serverValue!, options) - const setState = useSetLocalStorage(key, options) - - return [value, setState] as const -} - -export { useLocalStorage } diff --git a/web/hooks/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired.ts b/web/hooks/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired.ts deleted file mode 100644 index 227f4fd1fb4..00000000000 --- a/web/hooks/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as reactExports from 'react' -import { useCallback, useEffect, useLayoutEffect, useRef } from 'react' - -// useIsomorphicInsertionEffect -const useInsertionEffect - = typeof window === 'undefined' - // useInsertionEffect is only available in React 18+ - - ? useEffect - : reactExports.useInsertionEffect || useLayoutEffect - -/** - * @see https://foxact.skk.moe/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired - * Similar to useCallback, with a few subtle differences: - * - The returned function is a stable reference, and will always be the same between renders - * - No dependency lists required - * - Properties or state accessed within the callback will always be "current" - */ -// eslint-disable-next-line ts/no-explicit-any -export function useStableHandler( - callback: (...args: Args) => Result, -): typeof callback { - // Keep track of the latest callback: - // eslint-disable-next-line ts/no-explicit-any - const latestRef = useRef(shouldNotBeInvokedBeforeMount as any) - useInsertionEffect(() => { - latestRef.current = callback - }, [callback]) - - return useCallback((...args) => { - const fn = latestRef.current - return fn(...args) - }, []) -} - -/** - * Render methods should be pure, especially when concurrency is used, - * so we will throw this error if the callback is called while rendering. - */ -function shouldNotBeInvokedBeforeMount() { - throw new Error( - 'foxact: the stablized handler cannot be invoked before the component has mounted.', - ) -} diff --git a/web/hooks/use-typescript-happy-callback.ts b/web/hooks/use-typescript-happy-callback.ts deleted file mode 100644 index db3ba372c01..00000000000 --- a/web/hooks/use-typescript-happy-callback.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useCallback as useCallbackFromReact } from 'react' - -/** @see https://foxact.skk.moe/use-typescript-happy-callback */ -const useTypeScriptHappyCallback: ( - fn: (...args: Args) => R, - deps: React.DependencyList, -) => (...args: Args) => R = useCallbackFromReact - -/** @see https://foxact.skk.moe/use-typescript-happy-callback */ -export const useCallback = useTypeScriptHappyCallback diff --git a/web/package.json b/web/package.json index f6130ce2b37..73910ba17cf 100644 --- a/web/package.json +++ b/web/package.json @@ -80,7 +80,6 @@ "abcjs": "catalog:", "ahooks": "catalog:", "class-variance-authority": "catalog:", - "client-only": "catalog:", "cmdk": "catalog:", "copy-to-clipboard": "catalog:", "cron-parser": "catalog:", @@ -95,6 +94,7 @@ "emoji-mart": "catalog:", "es-toolkit": "catalog:", "fast-deep-equal": "catalog:", + "foxact": "catalog:", "fuse.js": "catalog:", "hast-util-to-jsx-runtime": "catalog:", "html-entities": "catalog:", diff --git a/web/vitest.setup.ts b/web/vitest.setup.ts index 99d264113e8..60e8d7c2495 100644 --- a/web/vitest.setup.ts +++ b/web/vitest.setup.ts @@ -76,7 +76,7 @@ afterEach(async () => { }) // mock custom clipboard hook - wraps writeTextToClipboard with fallback -vi.mock('@/hooks/use-clipboard', () => ({ +vi.mock('foxact/use-clipboard', () => ({ useClipboard: () => ({ copy: vi.fn(), copied: false,