4.8 KiB
4.8 KiB
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
// 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
// Create new array reference
setContainers(prev => prev.map(c =>
c.id === targetId
? { ...c, items: [...c.items, newItem] }
: c
));
Complete DragEnd Handler
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
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
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
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
const SortableItem = React.memo(({ item }: { item: Item }) => {
// Component implementation
});
const SortableDroppableContainer = React.memo(({
container
}: {
container: Container
}) => {
// Component implementation
});
Common Pitfalls
- Forgetting to spread arrays:
[...container.items, newItem]notcontainer.items.push(newItem) - Mutating nested objects: Always spread objects at every level
{ ...container, items: [...container.items] } - Not checking for null: Always verify
overexists before accessingover.id - Incorrect ID type checking: Use
.toString().startsWith()for reliable prefix checking
Testing State Updates
// 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');