193 lines
4.8 KiB
Markdown
193 lines
4.8 KiB
Markdown
# State Management Patterns
|
|
|
|
## Critical: Immutable State Updates
|
|
|
|
React requires new object references to detect state changes. **Always create new arrays/objects** when updating state.
|
|
|
|
### ❌ Common Mistakes
|
|
|
|
```tsx
|
|
// Direct mutation - won't trigger re-render
|
|
containers.items.push(newItem);
|
|
|
|
// Modifying existing object - won't trigger re-render
|
|
const container = containers.find(c => c.id === targetId);
|
|
container.items.push(newItem);
|
|
```
|
|
|
|
### ✅ Correct Patterns
|
|
|
|
```tsx
|
|
// Create new array reference
|
|
setContainers(prev => prev.map(c =>
|
|
c.id === targetId
|
|
? { ...c, items: [...c.items, newItem] }
|
|
: c
|
|
));
|
|
```
|
|
|
|
## Complete DragEnd Handler
|
|
|
|
```tsx
|
|
const handleDragEnd = (event: DragEndEvent) => {
|
|
const { active, over } = event;
|
|
|
|
if (!over) return;
|
|
|
|
const activeId = active.id.toString();
|
|
const overId = over.id.toString();
|
|
|
|
// Case 1: Reordering containers
|
|
if (activeId.startsWith('container-') && overId.startsWith('container-')) {
|
|
setContainers(prev => {
|
|
const oldIndex = prev.findIndex(c => c.id === activeId);
|
|
const newIndex = prev.findIndex(c => c.id === overId);
|
|
return arrayMove(prev, oldIndex, newIndex);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Case 2: Reordering items within same container
|
|
if (activeId.startsWith('item-') && overId.startsWith('item-')) {
|
|
setContainers(prev => prev.map(container => {
|
|
const activeItem = container.items.find(item => item.id === activeId);
|
|
const overItem = container.items.find(item => item.id === overId);
|
|
|
|
// Both items in this container
|
|
if (activeItem && overItem) {
|
|
const oldIndex = container.items.findIndex(i => i.id === activeId);
|
|
const newIndex = container.items.findIndex(i => i.id === overId);
|
|
|
|
return {
|
|
...container,
|
|
items: arrayMove(container.items, oldIndex, newIndex)
|
|
};
|
|
}
|
|
|
|
return container;
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// Case 3: Moving item to different container
|
|
if (activeId.startsWith('item-') && overId.startsWith('container-')) {
|
|
setContainers(prev => {
|
|
let itemToMove: Item | null = null;
|
|
|
|
// Remove item from source container
|
|
const afterRemoval = prev.map(container => {
|
|
const item = container.items.find(i => i.id === activeId);
|
|
if (item) {
|
|
itemToMove = item;
|
|
return {
|
|
...container,
|
|
items: container.items.filter(i => i.id !== activeId)
|
|
};
|
|
}
|
|
return container;
|
|
});
|
|
|
|
if (!itemToMove) return prev;
|
|
|
|
// Add item to target container
|
|
return afterRemoval.map(container =>
|
|
container.id === overId
|
|
? { ...container, items: [...container.items, itemToMove!] }
|
|
: container
|
|
);
|
|
});
|
|
}
|
|
};
|
|
```
|
|
|
|
## State Structure Recommendations
|
|
|
|
### Basic Structure
|
|
|
|
```tsx
|
|
interface Item {
|
|
id: string;
|
|
content: string;
|
|
// Additional item properties
|
|
}
|
|
|
|
interface Container {
|
|
id: string;
|
|
title: string;
|
|
items: Item[];
|
|
// Additional container properties
|
|
}
|
|
|
|
// State
|
|
const [containers, setContainers] = useState<Container[]>([]);
|
|
```
|
|
|
|
### With Optimistic Updates
|
|
|
|
```tsx
|
|
const [containers, setContainers] = useState<Container[]>([]);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
|
|
const handleDragStart = () => setIsDragging(true);
|
|
const handleDragEnd = (event: DragEndEvent) => {
|
|
setIsDragging(false);
|
|
// Handle state updates
|
|
};
|
|
```
|
|
|
|
## Performance Optimization
|
|
|
|
### Memoize Expensive Computations
|
|
|
|
```tsx
|
|
const containerIds = useMemo(
|
|
() => containers.map(c => c.id),
|
|
[containers]
|
|
);
|
|
|
|
const itemsByContainer = useMemo(
|
|
() => containers.reduce((acc, container) => {
|
|
acc[container.id] = container.items;
|
|
return acc;
|
|
}, {} as Record<string, Item[]>),
|
|
[containers]
|
|
);
|
|
```
|
|
|
|
### Avoid Re-renders with React.memo
|
|
|
|
```tsx
|
|
const SortableItem = React.memo(({ item }: { item: Item }) => {
|
|
// Component implementation
|
|
});
|
|
|
|
const SortableDroppableContainer = React.memo(({
|
|
container
|
|
}: {
|
|
container: Container
|
|
}) => {
|
|
// Component implementation
|
|
});
|
|
```
|
|
|
|
## Common Pitfalls
|
|
|
|
1. **Forgetting to spread arrays**: `[...container.items, newItem]` not `container.items.push(newItem)`
|
|
2. **Mutating nested objects**: Always spread objects at every level `{ ...container, items: [...container.items] }`
|
|
3. **Not checking for null**: Always verify `over` exists before accessing `over.id`
|
|
4. **Incorrect ID type checking**: Use `.toString().startsWith()` for reliable prefix checking
|
|
|
|
## Testing State Updates
|
|
|
|
```tsx
|
|
// Verify new reference is created
|
|
const beforeUpdate = containers;
|
|
setContainers(/* update */);
|
|
expect(containers).not.toBe(beforeUpdate); // Different reference
|
|
|
|
// Verify item was moved
|
|
expect(containers[0].items.length).toBe(2);
|
|
expect(containers[1].items.length).toBe(1);
|
|
expect(containers[1].items[0].id).toBe('item-1');
|
|
```
|