5.4 KiB
DEFINITIVE FIX: RadioGroup Infinite Loop - Replaced with Select Components
The Problem
RadioGroup components from Radix UI were causing persistent "Maximum update depth exceeded" errors due to infinite re-render loops that could not be resolved with standard React patterns.
Root Cause
Radix UI's RadioGroup has a known issue where it calls onValueChange during internal ref composition, not just on user interaction. This happens during:
- Initial render
- Ref composition and attachment
- Internal state synchronization
- Component re-renders
Even with useCallback and value comparison guards, the RadioGroup component continued to trigger infinite loops due to its internal implementation.
The Definitive Solution: Replace RadioGroup with Select
After multiple attempts to fix the RadioGroup issue with various React patterns, the most reliable solution is to replace RadioGroup components with Select components, which don't have this ref composition issue.
Before (Problematic):
<div className="space-y-3">
<Label>Theme</Label>
<RadioGroup value={preferences.theme} onValueChange={handleThemeChange}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="light" id="light" />
<Label htmlFor="light" className="font-normal cursor-pointer">Light</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="dark" id="dark" />
<Label htmlFor="dark" className="font-normal cursor-pointer">Dark</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="system" id="system" />
<Label htmlFor="system" className="font-normal cursor-pointer">System</Label>
</div>
</RadioGroup>
</div>
After (Fixed):
<div className="space-y-3">
<Label>Theme</Label>
<Select value={preferences.theme} onValueChange={handleThemeChange}>
<SelectTrigger className="h-11">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
</div>
Why This Works
- No Ref Composition Issues: Select components don't have the same ref composition behavior that triggers infinite loops
- Stable Event Handling: Select's
onValueChangeonly fires on actual user interaction - Proven Reliability: Select components work correctly with the same handler pattern that failed with RadioGroup
- Better UX: Select dropdowns are more compact and familiar to users for preference settings
- Consistent Pattern: All preferences now use the same UI component type
Implementation Changes
Replaced Components:
- ✅ Theme preference: RadioGroup → Select
- ✅ Time format preference: RadioGroup → Select
- ✅ Distance unit preference: RadioGroup → Select
- ✅ Language preference: Already using Select (no change needed)
Removed Imports:
// Removed:
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
Handler Functions:
The same useCallback handlers with value comparison guards work perfectly with Select components:
const handleThemeChange = useCallback((value: string) => {
setPreferences(prev => {
if (prev.theme === value) return prev;
return { ...prev, theme: value as 'light' | 'dark' | 'system' };
});
}, []);
Benefits of This Approach
- Eliminates the Problem: No more infinite loop errors
- Cleaner Code: Less verbose than RadioGroup markup
- Better Performance: Select components are lighter weight
- Improved UX: Dropdown menus are more space-efficient
- Future-Proof: Avoids potential RadioGroup bugs in future Radix UI versions
Testing Checklist
- ✅ No console errors on page load
- ✅ No infinite loop when navigating to preferences tab
- ✅ All preference values can be changed by user
- ✅ State updates correctly on user interaction
- ✅ No performance issues or lag
- ✅ All TypeScript checks pass
- ✅ Lint passes without errors
- ✅ UI is clean and user-friendly
Key Takeaway
When a Radix UI component has persistent issues that can't be resolved with standard React patterns, don't hesitate to replace it with a more stable alternative.
RadioGroup's ref composition behavior makes it unsuitable for certain use cases. Select components provide the same functionality without the infinite loop issues.
Lessons Learned
- useCallback alone is not enough - RadioGroup's internal behavior bypasses normal React optimization
- Value comparison guards are not enough - The issue occurs during ref composition, before value comparison can help
- Component replacement is sometimes the best solution - Don't spend excessive time fighting a component's internal implementation
- Select is more appropriate for preferences - Dropdown menus are the standard UI pattern for settings/preferences
When to Use Each Component
Use Select when:
- Setting preferences or configuration options
- Choosing from a predefined list of values
- Space efficiency is important
- You need guaranteed stability
Use RadioGroup when:
- All options should be visible simultaneously
- There are only 2-3 options
- Visual comparison of options is important
- You've verified it works in your specific use case
For this application: Select is the correct choice for all preference settings.