From fa548bbb1e79eb960ef56176ee12802bd159fd39 Mon Sep 17 00:00:00 2001 From: Brendan Whiting Date: Sat, 17 Jan 2026 11:01:55 -0800 Subject: [PATCH] Fix ModelSwitcher to derive selectedProviderId instead of setState in effect Replace the problematic useEffect that set state synchronously with a useMemo that derives the selected provider. This eliminates the double render and satisfies the react-hooks/set-state-in-effect rule without needing eslint-disable. Changes: - Renamed selectedProviderId state to manuallySelectedProviderId - Use useMemo to derive selectedProviderId from: 1. Manual selection (if user clicked a provider) 2. Current model's provider (if a model is selected) 3. First available provider (as fallback) - No more setState in useEffect = no cascading renders --- .../src/components/chat/ModelSwitcher.tsx | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/renderer/src/components/chat/ModelSwitcher.tsx b/src/renderer/src/components/chat/ModelSwitcher.tsx index 5b6331f..5d83578 100644 --- a/src/renderer/src/components/chat/ModelSwitcher.tsx +++ b/src/renderer/src/components/chat/ModelSwitcher.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo } from 'react' import { ChevronDown, Check, AlertCircle, Key } from 'lucide-react' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Button } from '@/components/ui/button' @@ -53,7 +53,9 @@ interface ModelSwitcherProps { export function ModelSwitcher({ threadId }: ModelSwitcherProps): React.JSX.Element { const [open, setOpen] = useState(false) - const [selectedProviderId, setSelectedProviderId] = useState(null) + const [manuallySelectedProviderId, setManuallySelectedProviderId] = useState( + null + ) const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false) const [apiKeyProvider, setApiKeyProvider] = useState(null) @@ -69,20 +71,22 @@ export function ModelSwitcher({ threadId }: ModelSwitcherProps): React.JSX.Eleme // Use fallback providers if none loaded const displayProviders = providers.length > 0 ? providers : FALLBACK_PROVIDERS - // Set initial selected provider based on current model - useEffect(() => { - if (!selectedProviderId && currentModel) { + // Derive the selected provider from current model or manual selection + const selectedProviderId = useMemo(() => { + // If user manually selected a provider, use that + if (manuallySelectedProviderId) { + return manuallySelectedProviderId + } + // Otherwise, derive from current model + if (currentModel) { const model = models.find((m) => m.id === currentModel) if (model) { - // eslint-disable-next-line react-hooks/set-state-in-effect - setSelectedProviderId(model.provider) + return model.provider } } - // Default to first provider if none selected - if (!selectedProviderId && displayProviders.length > 0) { - setSelectedProviderId(displayProviders[0].id) - } - }, [currentModel, models, selectedProviderId, displayProviders]) + // Default to first provider + return displayProviders[0]?.id || null + }, [manuallySelectedProviderId, currentModel, models, displayProviders]) const selectedModel = models.find((m) => m.id === currentModel) const filteredModels = selectedProviderId @@ -91,7 +95,7 @@ export function ModelSwitcher({ threadId }: ModelSwitcherProps): React.JSX.Eleme const selectedProvider = displayProviders.find((p) => p.id === selectedProviderId) function handleProviderClick(provider: Provider): void { - setSelectedProviderId(provider.id) + setManuallySelectedProviderId(provider.id) } function handleModelSelect(modelId: string): void {