refactor(web): drop swr remnants

This commit is contained in:
yyh 2025-12-27 13:03:18 +08:00
parent c4336dc560
commit 8875c7f646
No known key found for this signature in database
13 changed files with 24 additions and 62 deletions

View File

@ -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

View File

@ -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 }) => {
<>
<GA gaType={GaType.admin} />
<AmplitudeProvider />
<SwrInitializer>
<AppInitializer>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
@ -38,7 +38,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
</EventEmitterContextProvider>
</AppContextProvider>
<Zendesk />
</SwrInitializer>
</AppInitializer>
</>
)
}

View File

@ -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 }) => {
<>
<GA gaType={GaType.admin} />
<AmplitudeProvider />
<SwrInitor>
<AppInitializer>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
@ -30,7 +30,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
</ProviderContextProvider>
</EventEmitterContextProvider>
</AppContextProvider>
</SwrInitor>
</AppInitializer>
</>
)
}

View File

@ -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
? (
<SWRConfig value={{
shouldRetryOnError: false,
revalidateOnFocus: false,
dedupingInterval: 60000,
focusThrottleInterval: 5000,
provider: () => new Map(),
}}
>
{children}
</SWRConfig>
)
: null
return init ? children : null
}
export default SwrInitializer

View File

@ -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!',

View File

@ -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",

View File

@ -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:

View File

@ -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')
}

View File

@ -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,

View File

@ -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-<feature>.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) {

View File

@ -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<AnnotationCreateResponse>(`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}`)
}

View File

@ -12,7 +12,7 @@ export const fetchAppDetail = ({ url, id }: { url: string, id: string }): Promis
return get<AppDetailResponse>(`${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<AppDetailResponse> => {
return get<AppDetailResponse>(`${url}/${id}`)
}

View File

@ -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<MarketplaceCollectionsResponse, { url: string }> = ({ url }) => {
export const fetchMarketplaceCollections = ({ url }: { url: string }): Promise<MarketplaceCollectionsResponse> => {
return get<MarketplaceCollectionsResponse>(url)
}
export const fetchMarketplaceCollectionPlugins: Fetcher<MarketplaceCollectionPluginsResponse, { url: string }> = ({ url }) => {
export const fetchMarketplaceCollectionPlugins = ({ url }: { url: string }): Promise<MarketplaceCollectionPluginsResponse> => {
return get<MarketplaceCollectionPluginsResponse>(url)
}