72 lines
2.7 KiB
Markdown
72 lines
2.7 KiB
Markdown
# Solution: Keeping Dropdowns Open During Multiple Selections
|
|
|
|
## The Problem
|
|
|
|
When implementing a multi-select dropdown in React, a common issue occurs:
|
|
|
|
1. You select an item in the dropdown
|
|
2. The `onChange` handler is called, updating the data
|
|
3. This triggers a re-render of the parent component (in this case, the entire table)
|
|
4. During the re-render, the dropdown is unmounted and remounted
|
|
5. This causes the dropdown to close before you can make multiple selections
|
|
|
|
## The Solution: Deferred State Updates
|
|
|
|
The key insight is to **separate local state management from parent state updates**:
|
|
|
|
```typescript
|
|
// Step 1: Add local state to track selections
|
|
const [internalValue, setInternalValue] = useState<string[]>(value)
|
|
|
|
// Step 2: Handle popover open state changes
|
|
const handleOpenChange = useCallback((newOpen: boolean) => {
|
|
if (open && !newOpen) {
|
|
// Only update parent state when dropdown closes
|
|
if (JSON.stringify(internalValue) !== JSON.stringify(value)) {
|
|
onChange(internalValue);
|
|
}
|
|
}
|
|
|
|
setOpen(newOpen);
|
|
|
|
if (newOpen) {
|
|
// Sync internal state with external state when opening
|
|
setInternalValue(value);
|
|
}
|
|
}, [open, internalValue, value, onChange]);
|
|
|
|
// Step 3: Toggle selection only updates internal state
|
|
const toggleSelection = useCallback((selectedValue: string) => {
|
|
setInternalValue(prev => {
|
|
if (prev.includes(selectedValue)) {
|
|
return prev.filter(v => v !== selectedValue);
|
|
} else {
|
|
return [...prev, selectedValue];
|
|
}
|
|
});
|
|
}, []);
|
|
```
|
|
|
|
## Why This Works
|
|
|
|
1. **No parent re-renders during selection**: Since we're only updating local state, the parent component doesn't re-render during selection.
|
|
2. **Consistent UI**: The dropdown shows accurate selected states using the internal value.
|
|
3. **Data integrity**: The final selections are properly synchronized back to the parent when done.
|
|
4. **Resilient to external changes**: Initial state is synchronized when opening the dropdown.
|
|
|
|
## Implementation Steps
|
|
|
|
1. Create a local state variable to track selections inside the component
|
|
2. Only make selections against this local state while the dropdown is open
|
|
3. Defer updating the parent until the dropdown is explicitly closed
|
|
4. When opening, synchronize the internal state with the external value
|
|
|
|
## Benefits
|
|
|
|
This pattern:
|
|
- Avoids re-render cycles that would unmount the dropdown
|
|
- Maintains UI consistency during multi-selection
|
|
- Simplifies the component's interaction with parent components
|
|
- Works with existing component lifecycles rather than fighting against them
|
|
|
|
This solution is much simpler than trying to prevent event propagation or manipulating DOM events, and addresses the root cause of the issue: premature re-rendering. |