From 8875c7f64682185d52c6993653bd081c1a8d8060 Mon Sep 17 00:00:00 2001 From: yyh Date: Sat, 27 Dec 2025 13:03:18 +0800 Subject: [PATCH] refactor(web): drop swr remnants --- .claude/skills/component-refactoring/SKILL.md | 2 +- web/app/(commonLayout)/layout.tsx | 6 ++--- web/app/account/(commonLayout)/layout.tsx | 6 ++--- ...wr-initializer.tsx => app-initializer.tsx} | 25 ++++--------------- web/knip.config.ts | 2 +- web/package.json | 1 - web/pnpm-lock.yaml | 14 ----------- web/scripts/analyze-component.js | 3 +-- web/scripts/component-analyzer.js | 6 ++--- web/scripts/refactor-component.js | 9 ++----- web/service/annotation.ts | 5 ++-- web/service/apps.ts | 2 +- web/service/plugins.ts | 5 ++-- 13 files changed, 24 insertions(+), 62 deletions(-) rename web/app/components/{swr-initializer.tsx => app-initializer.tsx} (80%) diff --git a/.claude/skills/component-refactoring/SKILL.md b/.claude/skills/component-refactoring/SKILL.md index ea695ea442..7006c382c8 100644 --- a/.claude/skills/component-refactoring/SKILL.md +++ b/.claude/skills/component-refactoring/SKILL.md @@ -187,7 +187,7 @@ const Template = useMemo(() => { **When**: Component directly handles API calls, data transformation, or complex async operations. -**Dify Convention**: Use `@tanstack/react-query` hooks from `web/service/use-*.ts` or create custom data hooks. Project is migrating from SWR to React Query. +**Dify Convention**: Use `@tanstack/react-query` hooks from `web/service/use-*.ts` or create custom data hooks. ```typescript // ❌ Before: API logic in component diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx index 91bdb3f99a..a0ccde957d 100644 --- a/web/app/(commonLayout)/layout.tsx +++ b/web/app/(commonLayout)/layout.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react' import * as React from 'react' +import { AppInitializer } from '@/app/components/app-initializer' import AmplitudeProvider from '@/app/components/base/amplitude' import GA, { GaType } from '@/app/components/base/ga' import Zendesk from '@/app/components/base/zendesk' @@ -7,7 +8,6 @@ import GotoAnything from '@/app/components/goto-anything' import Header from '@/app/components/header' import HeaderWrapper from '@/app/components/header/header-wrapper' import ReadmePanel from '@/app/components/plugins/readme-panel' -import SwrInitializer from '@/app/components/swr-initializer' import { AppContextProvider } from '@/context/app-context' import { EventEmitterContextProvider } from '@/context/event-emitter' import { ModalContextProvider } from '@/context/modal-context' @@ -20,7 +20,7 @@ const Layout = ({ children }: { children: ReactNode }) => { <> - + @@ -38,7 +38,7 @@ const Layout = ({ children }: { children: ReactNode }) => { - + ) } diff --git a/web/app/account/(commonLayout)/layout.tsx b/web/app/account/(commonLayout)/layout.tsx index f264441b86..e4125015d9 100644 --- a/web/app/account/(commonLayout)/layout.tsx +++ b/web/app/account/(commonLayout)/layout.tsx @@ -1,9 +1,9 @@ import type { ReactNode } from 'react' import * as React from 'react' +import { AppInitializer } from '@/app/components/app-initializer' import AmplitudeProvider from '@/app/components/base/amplitude' import GA, { GaType } from '@/app/components/base/ga' import HeaderWrapper from '@/app/components/header/header-wrapper' -import SwrInitor from '@/app/components/swr-initializer' import { AppContextProvider } from '@/context/app-context' import { EventEmitterContextProvider } from '@/context/event-emitter' import { ModalContextProvider } from '@/context/modal-context' @@ -15,7 +15,7 @@ const Layout = ({ children }: { children: ReactNode }) => { <> - + @@ -30,7 +30,7 @@ const Layout = ({ children }: { children: ReactNode }) => { - + ) } diff --git a/web/app/components/swr-initializer.tsx b/web/app/components/app-initializer.tsx similarity index 80% rename from web/app/components/swr-initializer.tsx rename to web/app/components/app-initializer.tsx index 31be6d62b5..0f710abf39 100644 --- a/web/app/components/swr-initializer.tsx +++ b/web/app/components/app-initializer.tsx @@ -3,7 +3,6 @@ import type { ReactNode } from 'react' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { useCallback, useEffect, useState } from 'react' -import { SWRConfig } from 'swr' import { EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION, EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, @@ -11,12 +10,13 @@ import { import { fetchSetupStatus } from '@/service/common' import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect' -type SwrInitializerProps = { +type AppInitializerProps = { children: ReactNode } -const SwrInitializer = ({ + +export const AppInitializer = ({ children, -}: SwrInitializerProps) => { +}: AppInitializerProps) => { const router = useRouter() const searchParams = useSearchParams() // Tokens are now stored in cookies, no need to check localStorage @@ -69,20 +69,5 @@ const SwrInitializer = ({ })() }, [isSetupFinished, router, pathname, searchParams]) - return init - ? ( - new Map(), - }} - > - {children} - - ) - : null + return init ? children : null } - -export default SwrInitializer diff --git a/web/knip.config.ts b/web/knip.config.ts index 6151a78af7..a63f742a21 100644 --- a/web/knip.config.ts +++ b/web/knip.config.ts @@ -76,7 +76,7 @@ const config: KnipConfig = { // Browser initialization (runs on client startup) 'app/components/browser-initializer.tsx!', 'app/components/sentry-initializer.tsx!', - 'app/components/swr-initializer.tsx!', + 'app/components/app-initializer.tsx!', // i18n initialization (server and client) 'app/components/i18n.tsx!', diff --git a/web/package.json b/web/package.json index fe6b8ec9f7..d506bdaa0d 100644 --- a/web/package.json +++ b/web/package.json @@ -138,7 +138,6 @@ "semver": "^7.7.3", "sharp": "^0.33.5", "sortablejs": "^1.15.6", - "swr": "^2.3.6", "tailwind-merge": "^2.6.0", "tldts": "^7.0.17", "use-context-selector": "^2.0.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 51f421e90b..9677c14edd 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -330,9 +330,6 @@ importers: sortablejs: specifier: ^1.15.6 version: 1.15.6 - swr: - specifier: ^2.3.6 - version: 2.3.7(react@19.2.3) tailwind-merge: specifier: ^2.6.0 version: 2.6.0 @@ -7950,11 +7947,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swr@2.3.7: - resolution: {integrity: sha512-ZEquQ82QvalqTxhBVv/DlAg2mbmUjF4UgpPg9wwk4ufb9rQnZXh1iKyyKBqV6bQGu1Ie7L1QwSYO07qFIa1p+g==} - peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -17385,12 +17377,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swr@2.3.7(react@19.2.3): - dependencies: - dequal: 2.0.3 - react: 19.2.3 - use-sync-external-store: 1.6.0(react@19.2.3) - symbol-tree@3.2.4: {} synckit@0.11.11: diff --git a/web/scripts/analyze-component.js b/web/scripts/analyze-component.js index 8b01744b3d..9347a82fa5 100755 --- a/web/scripts/analyze-component.js +++ b/web/scripts/analyze-component.js @@ -46,7 +46,6 @@ Features Detected: ${analysis.hasEvents ? '✓' : '✗'} Event handlers ${analysis.hasRouter ? '✓' : '✗'} Next.js routing ${analysis.hasAPI ? '✓' : '✗'} API calls - ${analysis.hasSWR ? '✓' : '✗'} SWR data fetching ${analysis.hasReactQuery ? '✓' : '✗'} React Query ${analysis.hasAhooks ? '✓' : '✗'} ahooks ${analysis.hasForwardRef ? '✓' : '✗'} Ref forwarding (forwardRef) @@ -236,7 +235,7 @@ Create the test file at: ${testPath} // ===== API Calls ===== if (analysis.hasAPI) { guidelines.push('🌐 API calls detected:') - guidelines.push(' - Mock API calls/hooks (useSWR, useQuery, fetch, etc.)') + guidelines.push(' - Mock API calls/hooks (useQuery, useMutation, fetch, etc.)') guidelines.push(' - Test loading, success, and error states') guidelines.push(' - Focus on component behavior, not the data fetching lib') } diff --git a/web/scripts/component-analyzer.js b/web/scripts/component-analyzer.js index c53b652bc2..8bd3dc4409 100644 --- a/web/scripts/component-analyzer.js +++ b/web/scripts/component-analyzer.js @@ -21,6 +21,7 @@ export class ComponentAnalyzer { const resolvedPath = absolutePath ?? path.resolve(process.cwd(), filePath) const fileName = path.basename(filePath, path.extname(filePath)) const lineCount = code.split('\n').length + const hasReactQuery = /\buse(?:Query|Queries|InfiniteQuery|SuspenseQuery|SuspenseInfiniteQuery|Mutation)\b/.test(code) // Calculate complexity metrics const { total: rawComplexity, max: rawMaxComplexity } = this.calculateCognitiveComplexity(code) @@ -44,14 +45,13 @@ export class ComponentAnalyzer { hasMemo: code.includes('useMemo'), hasEvents: /on[A-Z]\w+/.test(code), hasRouter: code.includes('useRouter') || code.includes('usePathname'), - hasAPI: code.includes('service/') || code.includes('fetch(') || code.includes('useSWR'), + hasAPI: code.includes('service/') || code.includes('fetch(') || hasReactQuery, hasForwardRef: code.includes('forwardRef'), hasComponentMemo: /React\.memo|memo\(/.test(code), hasSuspense: code.includes('Suspense') || /\blazy\(/.test(code), hasPortal: code.includes('createPortal'), hasImperativeHandle: code.includes('useImperativeHandle'), - hasSWR: code.includes('useSWR'), - hasReactQuery: code.includes('useQuery') || code.includes('useMutation'), + hasReactQuery, hasAhooks: code.includes('from \'ahooks\''), complexity, maxComplexity, diff --git a/web/scripts/refactor-component.js b/web/scripts/refactor-component.js index f890540515..a054650ba3 100644 --- a/web/scripts/refactor-component.js +++ b/web/scripts/refactor-component.js @@ -123,7 +123,6 @@ Usage: ${analysis.usageCount} reference${analysis.usageCount !== 1 ${analysis.hasRouter ? '✓' : '✗'} Next.js routing ${analysis.hasAPI ? '✓' : '✗'} API calls ${analysis.hasReactQuery ? '✓' : '✗'} React Query - ${analysis.hasSWR ? '✓' : '✗'} SWR (should migrate to React Query) ${analysis.hasAhooks ? '✓' : '✗'} ahooks ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -150,7 +149,7 @@ ${this.buildRequirements(analysis)} Follow Dify project conventions: - Place extracted hooks in \`hooks/\` subdirectory or as \`use-.ts\` -- Use React Query (\`@tanstack/react-query\`) for data fetching, not SWR +- Use React Query (\`@tanstack/react-query\`) for data fetching - Follow existing patterns in \`web/service/use-*.ts\` for API hooks - Keep each new file under 300 lines - Maintain TypeScript strict typing @@ -173,12 +172,8 @@ After refactoring, verify: } // Priority 2: Extract API/data logic - if (analysis.hasAPI && (analysis.hasEffects || analysis.hasSWR)) { - if (analysis.hasSWR) { - actions.push('🔄 MIGRATE SWR TO REACT QUERY: Replace useSWR with useQuery from @tanstack/react-query') - } + if (analysis.hasAPI) actions.push('🌐 EXTRACT DATA HOOK: Move API calls and data fetching logic into a dedicated hook using React Query') - } // Priority 3: Split large components if (analysis.lineCount > 300) { diff --git a/web/service/annotation.ts b/web/service/annotation.ts index acdb944386..8a19425044 100644 --- a/web/service/annotation.ts +++ b/web/service/annotation.ts @@ -1,4 +1,3 @@ -import type { Fetcher } from 'swr' import type { AnnotationCreateResponse, AnnotationEnableStatus, AnnotationItemBasic, EmbeddingModelConfig } from '@/app/components/app/annotation/type' import { ANNOTATION_DEFAULT } from '@/config' import { del, get, post } from './base' @@ -44,11 +43,11 @@ export const addAnnotation = (appId: string, body: AnnotationItemBasic) => { return post(`apps/${appId}/annotations`, { body }) } -export const annotationBatchImport: Fetcher<{ job_id: string, job_status: string }, { url: string, body: FormData }> = ({ url, body }) => { +export const annotationBatchImport = ({ url, body }: { url: string, body: FormData }): Promise<{ job_id: string, job_status: string }> => { return post<{ job_id: string, job_status: string }>(url, { body }, { bodyStringify: false, deleteContentType: true }) } -export const checkAnnotationBatchImportProgress: Fetcher<{ job_id: string, job_status: string }, { jobID: string, appId: string }> = ({ jobID, appId }) => { +export const checkAnnotationBatchImportProgress = ({ jobID, appId }: { jobID: string, appId: string }): Promise<{ job_id: string, job_status: string }> => { return get<{ job_id: string, job_status: string }>(`/apps/${appId}/annotations/batch-import-status/${jobID}`) } diff --git a/web/service/apps.ts b/web/service/apps.ts index 1e3c93a33a..60052e27dd 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -12,7 +12,7 @@ export const fetchAppDetail = ({ url, id }: { url: string, id: string }): Promis return get(`${url}/${id}`) } -// Direct API call function for non-SWR usage +// Direct API call function for one-off usage export const fetchAppDetailDirect = async ({ url, id }: { url: string, id: string }): Promise => { return get(`${url}/${id}`) } diff --git a/web/service/plugins.ts b/web/service/plugins.ts index afaebf4fb5..cf05d8c20d 100644 --- a/web/service/plugins.ts +++ b/web/service/plugins.ts @@ -1,4 +1,3 @@ -import type { Fetcher } from 'swr' import type { MarketplaceCollectionPluginsResponse, MarketplaceCollectionsResponse, @@ -82,11 +81,11 @@ export const fetchPluginInfoFromMarketPlace = async ({ return getMarketplace<{ data: { plugin: PluginInfoFromMarketPlace, version: { version: string } } }>(`/plugins/${org}/${name}`) } -export const fetchMarketplaceCollections: Fetcher = ({ url }) => { +export const fetchMarketplaceCollections = ({ url }: { url: string }): Promise => { return get(url) } -export const fetchMarketplaceCollectionPlugins: Fetcher = ({ url }) => { +export const fetchMarketplaceCollectionPlugins = ({ url }: { url: string }): Promise => { return get(url) }