fix: memoize provider selectors to prevent infinite re-renders (#12550)

The normalizeProvider() calls inside useAppSelector callbacks were
creating new object references on every render, causing infinite
re-render loops in components that depend on provider objects.

This fix uses createSelector from Redux Toolkit to properly memoize
the normalized providers, preventing unnecessary re-renders in:
- GithubCopilotSettings (useEffect depends on provider)
- Painting pages (useAllProviders returns new array each render)

Fixes the "Maximum update depth exceeded" error introduced in 8186d4fa.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
SuYao 2026-01-22 14:27:52 +08:00 committed by GitHub
parent f84941f91f
commit 1f209dc280
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit'
import { CHERRYAI_PROVIDER } from '@renderer/config/providers'
import { getDefaultProvider } from '@renderer/services/AssistantService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { type RootState, useAppDispatch, useAppSelector } from '@renderer/store'
import {
addModel,
addProvider,
@ -14,6 +14,7 @@ import {
import type { Assistant, Model, Provider } from '@renderer/types'
import { isSystemProvider } from '@renderer/types'
import { withoutTrailingSlash } from '@renderer/utils/api'
import { useMemo } from 'react'
import { useDefaultModel } from './useAssistant'
@ -28,13 +29,27 @@ function normalizeProvider<T extends Provider>(provider: T): T {
}
}
const selectEnabledProviders = createSelector(
(state) => state.llm.providers,
(providers) =>
providers
.map(normalizeProvider)
.filter((p) => p.enabled)
.concat(CHERRYAI_PROVIDER)
const selectProviders = (state: RootState) => state.llm.providers
const selectEnabledProviders = createSelector(selectProviders, (providers) =>
providers
.map(normalizeProvider)
.filter((p) => p.enabled)
.concat(CHERRYAI_PROVIDER)
)
const selectSystemProviders = createSelector(selectProviders, (providers) =>
providers.filter((p) => isSystemProvider(p)).map(normalizeProvider)
)
const selectUserProviders = createSelector(selectProviders, (providers) =>
providers.filter((p) => !isSystemProvider(p)).map(normalizeProvider)
)
const selectAllProviders = createSelector(selectProviders, (providers) => providers.map(normalizeProvider))
const selectAllProvidersWithCherryAI = createSelector(selectProviders, (providers) =>
[...providers, CHERRYAI_PROVIDER].map(normalizeProvider)
)
export function useProviders() {
@ -51,25 +66,20 @@ export function useProviders() {
}
export function useSystemProviders() {
return useAppSelector((state) => state.llm.providers.filter((p) => isSystemProvider(p)).map(normalizeProvider))
return useAppSelector(selectSystemProviders)
}
export function useUserProviders() {
return useAppSelector((state) => state.llm.providers.filter((p) => !isSystemProvider(p)).map(normalizeProvider))
return useAppSelector(selectUserProviders)
}
export function useAllProviders() {
return useAppSelector((state) => state.llm.providers.map(normalizeProvider))
return useAppSelector(selectAllProviders)
}
export function useProvider(id: string) {
const provider =
useAppSelector((state) =>
state.llm.providers
.concat([CHERRYAI_PROVIDER])
.map(normalizeProvider)
.find((p) => p.id === id)
) || getDefaultProvider()
const allProviders = useAppSelector(selectAllProvidersWithCherryAI)
const provider = useMemo(() => allProviders.find((p) => p.id === id) || getDefaultProvider(), [allProviders, id])
const dispatch = useAppDispatch()
return {