From 055d9b9f0acfbf29a7f0bf57bb6c44596e0dbc95 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:20:13 +0800 Subject: [PATCH] refactor(web): migrate local storage hook usage (#36890) --- eslint-suppressions.json | 176 ++++++++++++++++++ .../base/chat/chat-with-history/hooks.tsx | 6 +- .../base/chat/embedded-chatbot/hooks.tsx | 6 +- web/app/education-apply/hooks.ts | 16 +- web/eslint.config.mjs | 39 ++++ 5 files changed, 223 insertions(+), 20 deletions(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 83ef4074b1..bbc7eb7f50 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -109,6 +109,9 @@ } }, "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx": { + "no-restricted-globals": { + "count": 1 + }, "react/set-state-in-effect": { "count": 2 }, @@ -116,6 +119,11 @@ "count": 1 } }, + "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/__tests__/svg-attribute-error-reproduction.spec.tsx": { "no-console": { "count": 19 @@ -163,6 +171,9 @@ } }, "web/app/(shareLayout)/webapp-reset-password/page.tsx": { + "no-restricted-globals": { + "count": 1 + }, "no-restricted-imports": { "count": 1 } @@ -178,6 +189,9 @@ } }, "web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx": { + "no-restricted-globals": { + "count": 1 + }, "no-restricted-imports": { "count": 1 } @@ -213,6 +227,11 @@ "count": 1 } }, + "web/app/account/(commonLayout)/delete-account/index.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "web/app/account/oauth/authorize/layout.tsx": { "ts/no-explicit-any": { "count": 1 @@ -233,7 +252,15 @@ "count": 4 } }, + "web/app/components/app-sidebar/app-info/use-app-info-actions.ts": { + "no-restricted-globals": { + "count": 1 + } + }, "web/app/components/app-sidebar/index.tsx": { + "no-restricted-globals": { + "count": 2 + }, "ts/no-explicit-any": { "count": 1 } @@ -390,6 +417,9 @@ } }, "web/app/components/app/configuration/config/automatic/get-automatic-res.tsx": { + "no-restricted-globals": { + "count": 6 + }, "react/set-state-in-effect": { "count": 4 }, @@ -418,6 +448,9 @@ } }, "web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx": { + "no-restricted-globals": { + "count": 6 + }, "react/set-state-in-effect": { "count": 4 }, @@ -487,6 +520,9 @@ } }, "web/app/components/app/configuration/debug/hooks.tsx": { + "no-restricted-globals": { + "count": 2 + }, "ts/no-explicit-any": { "count": 3 } @@ -515,6 +551,9 @@ } }, "web/app/components/app/create-app-dialog/app-list/index.tsx": { + "no-restricted-globals": { + "count": 1 + }, "no-restricted-imports": { "count": 1 } @@ -528,6 +567,9 @@ } }, "web/app/components/app/create-app-modal/index.tsx": { + "no-restricted-globals": { + "count": 1 + }, "no-restricted-imports": { "count": 1 } @@ -536,6 +578,9 @@ "erasable-syntax-only/enums": { "count": 1 }, + "no-restricted-globals": { + "count": 2 + }, "no-restricted-imports": { "count": 1 }, @@ -601,6 +646,9 @@ } }, "web/app/components/app/switch-app-modal/index.tsx": { + "no-restricted-globals": { + "count": 1 + }, "no-restricted-imports": { "count": 1 }, @@ -636,7 +684,15 @@ "count": 1 } }, + "web/app/components/apps/app-card.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "web/app/components/apps/list.tsx": { + "no-restricted-globals": { + "count": 2 + }, "no-restricted-imports": { "count": 1 } @@ -788,6 +844,9 @@ } }, "web/app/components/base/chat/chat-with-history/hooks.tsx": { + "no-restricted-globals": { + "count": 2 + }, "react/set-state-in-effect": { "count": 4 }, @@ -1835,6 +1894,11 @@ "count": 4 } }, + "web/app/components/billing/plan/index.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "web/app/components/billing/pricing/assets/index.tsx": { "no-barrel-files/no-barrel-files": { "count": 12 @@ -2293,6 +2357,9 @@ } }, "web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts": { + "no-restricted-globals": { + "count": 2 + }, "react/set-state-in-effect": { "count": 1 } @@ -2315,6 +2382,11 @@ "count": 1 } }, + "web/app/components/datasets/metadata/metadata-document/info-group.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "web/app/components/datasets/metadata/types.ts": { "erasable-syntax-only/enums": { "count": 2 @@ -2358,6 +2430,11 @@ "count": 2 } }, + "web/app/components/education-verify-action-recorder.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "web/app/components/explore/app-list/index.tsx": { "no-restricted-imports": { "count": 1 @@ -2430,6 +2507,11 @@ "count": 1 } }, + "web/app/components/goto-anything/actions/recent-store.ts": { + "no-restricted-globals": { + "count": 2 + } + }, "web/app/components/goto-anything/actions/types.ts": { "ts/no-explicit-any": { "count": 2 @@ -2473,6 +2555,11 @@ "count": 1 } }, + "web/app/components/header/account-dropdown/index.tsx": { + "no-restricted-globals": { + "count": 3 + } + }, "web/app/components/header/account-setting/data-source-page-new/card.tsx": { "ts/no-explicit-any": { "count": 2 @@ -3151,6 +3238,11 @@ "count": 2 } }, + "web/app/components/signin/countdown.tsx": { + "no-restricted-globals": { + "count": 4 + } + }, "web/app/components/tools/edit-custom-collection-modal/get-schema.tsx": { "no-restricted-imports": { "count": 1 @@ -3321,6 +3413,16 @@ "count": 1 } }, + "web/app/components/workflow/block-selector/featured-tools.tsx": { + "no-restricted-properties": { + "count": 2 + } + }, + "web/app/components/workflow/block-selector/featured-triggers.tsx": { + "no-restricted-properties": { + "count": 2 + } + }, "web/app/components/workflow/block-selector/hooks.ts": { "react/set-state-in-effect": { "count": 1 @@ -3342,6 +3444,9 @@ } }, "web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx": { + "no-restricted-properties": { + "count": 3 + }, "react/set-state-in-effect": { "count": 1 } @@ -3463,6 +3568,11 @@ "count": 1 } }, + "web/app/components/workflow/hooks/use-workflow-canvas-maximize.ts": { + "no-restricted-globals": { + "count": 1 + } + }, "web/app/components/workflow/hooks/use-workflow-interactions.ts": { "no-barrel-files/no-barrel-files": { "count": 5 @@ -3693,6 +3803,9 @@ } }, "web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx": { + "no-restricted-globals": { + "count": 1 + }, "react/set-state-in-effect": { "count": 2 }, @@ -4193,6 +4306,11 @@ "count": 2 } }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx": { + "no-restricted-properties": { + "count": 3 + } + }, "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx": { "react-refresh/only-export-components": { "count": 2 @@ -4324,6 +4442,11 @@ "count": 9 } }, + "web/app/components/workflow/nodes/question-classifier/components/class-list.tsx": { + "no-restricted-properties": { + "count": 2 + } + }, "web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts": { "ts/no-explicit-any": { "count": 8 @@ -4508,6 +4631,11 @@ "count": 5 } }, + "web/app/components/workflow/note-node/hooks.ts": { + "no-restricted-globals": { + "count": 1 + } + }, "web/app/components/workflow/note-node/note-editor/index.tsx": { "no-barrel-files/no-barrel-files": { "count": 3 @@ -4535,6 +4663,9 @@ } }, "web/app/components/workflow/operator/hooks.ts": { + "no-restricted-globals": { + "count": 1 + }, "ts/no-explicit-any": { "count": 1 } @@ -4616,6 +4747,11 @@ "count": 12 } }, + "web/app/components/workflow/panel/debug-and-preview/index.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "web/app/components/workflow/panel/env-panel/variable-modal.tsx": { "no-restricted-imports": { "count": 1 @@ -4750,6 +4886,11 @@ "count": 2 } }, + "web/app/components/workflow/store/workflow/layout-slice.ts": { + "no-restricted-properties": { + "count": 1 + } + }, "web/app/components/workflow/store/workflow/workflow-draft-slice.ts": { "ts/no-explicit-any": { "count": 1 @@ -4826,6 +4967,11 @@ "count": 2 } }, + "web/app/components/workflow/variable-inspect/index.tsx": { + "no-restricted-globals": { + "count": 1 + } + }, "web/app/components/workflow/variable-inspect/left.tsx": { "ts/no-explicit-any": { "count": 1 @@ -4926,6 +5072,9 @@ } }, "web/app/reset-password/page.tsx": { + "no-restricted-globals": { + "count": 1 + }, "no-restricted-imports": { "count": 1 } @@ -4941,6 +5090,9 @@ } }, "web/app/signin/components/mail-and-code-auth.tsx": { + "no-restricted-globals": { + "count": 1 + }, "no-restricted-imports": { "count": 1 } @@ -4999,11 +5151,22 @@ } }, "web/context/hooks/use-trigger-events-limit-modal.ts": { + "no-restricted-globals": { + "count": 2 + }, "react/set-state-in-effect": { "count": 3 } }, + "web/context/modal-context-provider.tsx": { + "no-restricted-globals": { + "count": 2 + } + }, "web/context/provider-context-provider.tsx": { + "no-restricted-globals": { + "count": 2 + }, "ts/no-explicit-any": { "count": 1 } @@ -5026,6 +5189,11 @@ "count": 1 } }, + "web/hooks/use-import-dsl.ts": { + "no-restricted-globals": { + "count": 2 + } + }, "web/hooks/use-metadata.ts": { "ts/no-explicit-any": { "count": 1 @@ -5318,6 +5486,11 @@ "count": 1 } }, + "web/service/refresh-token.ts": { + "no-restricted-properties": { + "count": 7 + } + }, "web/service/share.ts": { "erasable-syntax-only/enums": { "count": 1 @@ -5476,6 +5649,9 @@ } }, "web/service/webapp-auth.ts": { + "no-restricted-globals": { + "count": 6 + }, "no-restricted-imports": { "count": 1 } 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 223c35f6dd..ae5692a56e 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -4,7 +4,6 @@ import type { InstalledApp } from '@/models/explore' import type { AppData, ConversationItem } from '@/models/share' import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow' import { toast } from '@langgenius/dify-ui/toast' -import { useLocalStorageState } from 'ahooks' import { noop } from 'es-toolkit/function' import { produce } from 'immer' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -13,6 +12,7 @@ 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' @@ -141,9 +141,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { } } }, [appId, setSidebarCollapseState]) - const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState>>(CONVERSATION_ID_INFO, { - defaultValue: {}, - }) + const [conversationIdInfo, setConversationIdInfo] = useLocalStorage>>(CONVERSATION_ID_INFO, {}) const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || '', [appId, conversationIdInfo, userId]) const handleConversationIdInfoChange = useCallback((changeConversationId: string) => { if (appId) { diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 39cbe66fbb..948be4ae3f 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -4,7 +4,6 @@ import type { InputValueTypes } from '@/app/components/share/text-generation/typ import type { Locale } from '@/i18n-config' import type { AppData, ConversationItem } from '@/models/share' import { toast } from '@langgenius/dify-ui/toast' -import { useLocalStorageState } from 'ahooks' import { noop } from 'es-toolkit/function' import { produce } from 'immer' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -12,6 +11,7 @@ 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' @@ -102,9 +102,7 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri } setLanguageFromParams() }, [appInfo]) - const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState>>(CONVERSATION_ID_INFO, { - defaultValue: {}, - }) + const [conversationIdInfo, setConversationIdInfo] = useLocalStorage>>(CONVERSATION_ID_INFO, {}) const removeConversationIdInfo = useCallback((appId: string) => { setConversationIdInfo((prev) => { const newInfo = { ...prev } diff --git a/web/app/education-apply/hooks.ts b/web/app/education-apply/hooks.ts index 740bcd99eb..0c7802b592 100644 --- a/web/app/education-apply/hooks.ts +++ b/web/app/education-apply/hooks.ts @@ -1,5 +1,5 @@ import type { SearchParams } from './types' -import { useDebounceFn, useLocalStorageState } from 'ahooks' +import { useDebounceFn } from 'ahooks' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' @@ -85,17 +85,10 @@ const useEducationReverifyNotice = ({ // const [educationInfo, setEducationInfo] = useState<{ is_student: boolean, allow_refresh: boolean, expire_at: number | null } | null>(null) // const isLoading = !educationInfo const { educationAccountExpireAt, allowRefreshEducationVerify, isLoadingEducationAccountInfo: isLoading } = useProviderContext() - const [prevExpireAt, setPrevExpireAt] = useLocalStorageState('education-reverify-prev-expire-at', { - defaultValue: 0, - }) - const [reverifyHasNoticed, setReverifyHasNoticed] = useLocalStorageState('education-reverify-has-noticed', { - defaultValue: false, - }) - const [expiredHasNoticed, setExpiredHasNoticed] = useLocalStorageState('education-expired-has-noticed', { - defaultValue: false, - }) + const [prevExpireAt, setPrevExpireAt] = useLocalStorage('education-reverify-prev-expire-at', 0) + const [reverifyHasNoticed, setReverifyHasNoticed] = useLocalStorage('education-reverify-has-noticed', false) + const [expiredHasNoticed, setExpiredHasNoticed] = useLocalStorage('education-expired-has-noticed', false) - /* eslint-disable react/set-state-in-effect -- this persists education notice acknowledgement after provider metadata changes. */ useEffect(() => { if (isLoading || !timezone) return @@ -124,7 +117,6 @@ const useEducationReverifyNotice = ({ } } }, [allowRefreshEducationVerify, timezone]) - /* eslint-enable react/set-state-in-effect */ return { isLoading, diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 9570f7c49c..3e5b621368 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -179,4 +179,43 @@ export default antfu( }], }, }, + { + name: 'dify/restricted-local-storage-access', + files: [GLOB_TS, GLOB_TSX], + ignores: [ + ...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.', + }, + ], + 'no-restricted-properties': [ + 'error', + { + object: 'window', + property: 'localStorage', + message: 'Do not use window.localStorage directly. Use @/hooks/use-local-storage instead.', + }, + { + object: 'globalThis', + property: 'localStorage', + message: 'Do not use globalThis.localStorage directly. Use @/hooks/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.', + }, + ], + }, + }, )