140 lines
5.4 KiB
Markdown
140 lines
5.4 KiB
Markdown
# 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):
|
|
```tsx
|
|
<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):
|
|
```tsx
|
|
<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
|
|
|
|
1. **No Ref Composition Issues:** Select components don't have the same ref composition behavior that triggers infinite loops
|
|
2. **Stable Event Handling:** Select's `onValueChange` only fires on actual user interaction
|
|
3. **Proven Reliability:** Select components work correctly with the same handler pattern that failed with RadioGroup
|
|
4. **Better UX:** Select dropdowns are more compact and familiar to users for preference settings
|
|
5. **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:
|
|
```tsx
|
|
// Removed:
|
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|
```
|
|
|
|
### Handler Functions:
|
|
The same `useCallback` handlers with value comparison guards work perfectly with Select components:
|
|
|
|
```tsx
|
|
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
|
|
|
|
1. **Eliminates the Problem:** No more infinite loop errors
|
|
2. **Cleaner Code:** Less verbose than RadioGroup markup
|
|
3. **Better Performance:** Select components are lighter weight
|
|
4. **Improved UX:** Dropdown menus are more space-efficient
|
|
5. **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
|
|
|
|
1. **useCallback alone is not enough** - RadioGroup's internal behavior bypasses normal React optimization
|
|
2. **Value comparison guards are not enough** - The issue occurs during ref composition, before value comparison can help
|
|
3. **Component replacement is sometimes the best solution** - Don't spend excessive time fighting a component's internal implementation
|
|
4. **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.
|