refactor(web): optimize model provider re-render and remove useEffect state sync

- Replace useEffect state sync with derived state pattern in useSystemDefaultModelAndModelList
- Use useCallback instead of useMemo for function memoization in useProviderCredentialsAndLoadBalancing
- Add memo() to ProviderAddedCard and CredentialPanel to prevent unnecessary re-renders
- Switch to useProviderContextSelector for precise context subscription in ProviderAddedCard
- Stabilize activate callback ref in useActivateCredential via supportedModelTypes ref
- Add usage priority tooltip with i18n support
This commit is contained in:
yyh 2026-03-05 15:07:53 +08:00
parent 7471c32612
commit 1752edc047
No known key found for this signature in database
9 changed files with 45 additions and 22 deletions

View File

@ -57,15 +57,21 @@ export const useSystemDefaultModelAndModelList: UseDefaultModelAndModelList = (
return currentDefaultModel
}, [defaultModel, modelList])
const currentDefaultModelKey = currentDefaultModel
? `${currentDefaultModel.provider}:${currentDefaultModel.model}`
: ''
const [defaultModelState, setDefaultModelState] = useState<DefaultModel | undefined>(currentDefaultModel)
const handleDefaultModelChange = useCallback((model: DefaultModel) => {
setDefaultModelState(model)
}, [])
useEffect(() => {
setDefaultModelState(currentDefaultModel)
}, [currentDefaultModel])
const [defaultModelSourceKey, setDefaultModelSourceKey] = useState(currentDefaultModelKey)
const selectedDefaultModel = defaultModelSourceKey === currentDefaultModelKey
? defaultModelState
: currentDefaultModel
return [defaultModelState, handleDefaultModelChange]
const handleDefaultModelChange = useCallback((model: DefaultModel) => {
setDefaultModelSourceKey(currentDefaultModelKey)
setDefaultModelState(model)
}, [currentDefaultModelKey])
return [selectedDefaultModel, handleDefaultModelChange]
}
export const useLanguage = () => {
@ -116,7 +122,7 @@ export const useProviderCredentialsAndLoadBalancing = (
predefinedFormSchemasValue?.credentials,
])
const mutate = useMemo(() => () => {
const mutate = useCallback(() => {
if (predefinedEnabled)
queryClient.invalidateQueries({ queryKey: ['model-providers', 'credentials', provider, credentialId] })
if (customEnabled)

View File

@ -4,6 +4,7 @@ import type {
} from '../declarations'
import type { CardVariant } from './use-credential-panel-state'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import Warning from '@/app/components/base/icons/src/vender/line/alertsAndFeedback/Warning'
import Toast from '@/app/components/base/toast'
@ -41,10 +42,11 @@ const CredentialPanel = ({
const updateModelList = useUpdateModelList()
const updateModelProviders = useUpdateModelProviders()
const state = useCredentialPanelState(provider)
const providerName = provider.provider
const modelProviderModelListQueryKey = consoleQuery.modelProviders.models.queryKey({
input: {
params: {
provider: provider.provider,
provider: providerName,
},
},
})
@ -72,7 +74,7 @@ const CredentialPanel = ({
const handleChangePriority = (key: PreferredProviderTypeEnum) => {
changePriority({
params: { provider: provider.provider },
params: { provider: providerName },
body: { preferred_provider_type: key },
})
}
@ -156,4 +158,4 @@ function StatusLabel({ variant, credentialName }: {
)
}
export default CredentialPanel
export default memo(CredentialPanel)

View File

@ -6,7 +6,7 @@ import type { ModelProviderQuotaGetPaid } from '../utils'
import type { PluginDetail } from '@/app/components/plugins/types'
import { useQuery } from '@tanstack/react-query'
import { useCallback } from 'react'
import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import {
AddCustomModel,
@ -14,7 +14,7 @@ import {
} from '@/app/components/header/account-setting/model-provider-page/model-auth'
import { IS_CE_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { useProviderContextSelector } from '@/context/provider-context'
import { consoleQuery } from '@/service/client'
import { cn } from '@/utils/classnames'
import { useModelProviderListExpanded, useSetModelProviderListExpanded } from '../atoms'
@ -40,7 +40,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
pluginDetail,
}) => {
const { t } = useTranslation()
const { refreshModelProviders } = useProviderContext()
const refreshModelProviders = useProviderContextSelector(state => state.refreshModelProviders)
const currentProviderName = provider.provider
const expanded = useModelProviderListExpanded(currentProviderName)
const setExpanded = useSetModelProviderListExpanded(currentProviderName)
@ -183,4 +183,4 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
)
}
export default ProviderAddedCard
export default memo(ProviderAddedCard)

View File

@ -1,5 +1,6 @@
import type { UsagePriority } from '../use-credential-panel-state'
import { useTranslation } from 'react-i18next'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
import { cn } from '@/utils/classnames'
import { PreferredProviderTypeEnum } from '../../declarations'
@ -30,7 +31,20 @@ export default function UsagePrioritySection({ value, disabled, onSelect }: Usag
<span className="truncate text-text-secondary system-sm-medium">
{t('modelProvider.card.usagePriority', { ns: 'common' })}
</span>
<span className="i-ri-question-line h-3.5 w-3.5 shrink-0 text-text-quaternary" />
<Tooltip>
<TooltipTrigger
aria-label={t('modelProvider.card.usagePriorityTip', { ns: 'common' })}
delay={0}
render={(
<span className="flex h-4 w-4 shrink-0 items-center justify-center">
<span aria-hidden className="i-ri-question-line h-3.5 w-3.5 text-text-quaternary hover:text-text-tertiary" />
</span>
)}
/>
<TooltipContent>
{t('modelProvider.card.usagePriorityTip', { ns: 'common' })}
</TooltipContent>
</Tooltip>
</div>
<div className="flex shrink-0 items-center gap-1">
{options.map(option => (

View File

@ -21,7 +21,8 @@ export function useActivateCredential(provider: ModelProvider) {
const selectedIdRef = useRef(selectedCredentialId)
selectedIdRef.current = selectedCredentialId
const supportedModelTypes = provider.supported_model_types
const supportedModelTypesRef = useRef(provider.supported_model_types)
supportedModelTypesRef.current = provider.supported_model_types
const activate = useCallback((credential: Credential) => {
if (credential.credential_id === selectedIdRef.current)
@ -33,7 +34,7 @@ export function useActivateCredential(provider: ModelProvider) {
onSuccess: () => {
Toast.notify({ type: 'success', message: t('api.actionSuccess', { ns: 'common' }) })
updateModelProviders()
supportedModelTypes.forEach(type => updateModelList(type))
supportedModelTypesRef.current.forEach(type => updateModelList(type))
},
onError: () => {
setOptimisticId(undefined)
@ -41,7 +42,7 @@ export function useActivateCredential(provider: ModelProvider) {
},
},
)
}, [mutate, t, updateModelProviders, updateModelList, supportedModelTypes])
}, [mutate, t, updateModelProviders, updateModelList])
return {
selectedCredentialId,

View File

@ -4672,9 +4672,6 @@
}
},
"app/components/header/account-setting/model-provider-page/hooks.ts": {
"react-hooks-extra/no-direct-set-state-in-use-effect": {
"count": 1
},
"ts/no-explicit-any": {
"count": 2
}

View File

@ -371,6 +371,7 @@
"modelProvider.card.upgradePlan": "upgrade your plan",
"modelProvider.card.usageLabel": "Usage",
"modelProvider.card.usagePriority": "Usage Priority",
"modelProvider.card.usagePriorityTip": "Set which resource to use first when running models.",
"modelProvider.collapse": "Collapse",
"modelProvider.config": "Config",
"modelProvider.configLoadBalancing": "Config Load Balancing",

View File

@ -353,6 +353,7 @@
"modelProvider.card.removeKey": "API キーを削除",
"modelProvider.card.tip": "メッセージ枠は{{modelNames}}のモデルを使用することをサポートしています。無料枠は有料枠が使い果たされた後に消費されます。",
"modelProvider.card.tokens": "トークン",
"modelProvider.card.usagePriorityTip": "モデル実行時に優先して使用するリソースを設定します。",
"modelProvider.collapse": "折り畳み",
"modelProvider.config": "設定",
"modelProvider.configLoadBalancing": "負荷分散の設定",

View File

@ -371,6 +371,7 @@
"modelProvider.card.upgradePlan": "升级套餐",
"modelProvider.card.usageLabel": "用量",
"modelProvider.card.usagePriority": "使用优先级",
"modelProvider.card.usagePriorityTip": "设置运行模型时优先使用的资源。",
"modelProvider.collapse": "收起",
"modelProvider.config": "配置",
"modelProvider.configLoadBalancing": "设置负载均衡",