Fix O(n²) lookup, memoize components and callbacks in QueueSection

- Build a participantMap (Map<id, participant>) with useMemo so each
  queue item lookup is O(1) instead of O(n) per render
- Memoize queueIds array for SortableContext to avoid churn
- Wrap handleDragEnd in useCallback so DndContext gets a stable reference
- Wrap SortableQueueItem in memo so it skips re-renders when props haven't
  changed (important since the parent re-renders every second during a
  live draft from timer-update socket events)
- Pass onRemoveFromQueue and onMakePick directly as props instead of
  creating new arrow functions per item per render; SortableQueueItem
  now calls them with the relevant id itself

https://claude.ai/code/session_01HPtNkL5m9xhYgtzWaGViSC
This commit is contained in:
Claude 2026-03-20 22:54:28 +00:00
parent 21119abf12
commit c499fb9595
No known key found for this signature in database

View file

@ -1,4 +1,4 @@
import { memo } from "react";
import { memo, useCallback, useMemo } from "react";
import { GripVertical } from "lucide-react";
import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
@ -47,7 +47,7 @@ interface QueueSectionProps {
}
// Sortable queue item component
function SortableQueueItem({
const SortableQueueItem = memo(function SortableQueueItem({
item,
index,
participantName,
@ -61,8 +61,8 @@ function SortableQueueItem({
participantName: string;
sportName?: string;
canPick: boolean;
onRemove: () => void;
onDraft?: () => void;
onRemove: (queueId: string) => void;
onDraft?: (participantId: string) => void;
}) {
const {
attributes,
@ -109,7 +109,7 @@ function SortableQueueItem({
variant="default"
size="sm"
className="h-7 text-xs bg-electric text-background hover:bg-electric/90"
onClick={onDraft}
onClick={() => onDraft(item.participantId)}
>
Draft
</Button>
@ -118,7 +118,7 @@ function SortableQueueItem({
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={onRemove}
onClick={() => onRemove(item.id)}
title="Remove from queue"
>
<span className="text-lg">×</span>
@ -126,7 +126,7 @@ function SortableQueueItem({
</div>
</div>
);
}
});
export const QueueSection = memo(function QueueSection({
queue,
@ -150,18 +150,25 @@ export const QueueSection = memo(function QueueSection({
})
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
const participantMap = useMemo(
() => new Map(availableParticipants.map((p) => [p.id, p])),
[availableParticipants]
);
const queueIds = useMemo(() => queue.map((item) => item.id), [queue]);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = queue.findIndex((item) => item.id === active.id);
const newIndex = queue.findIndex((item) => item.id === over.id);
const reorderedQueue = arrayMove(queue, oldIndex, newIndex);
const participantIds = reorderedQueue.map((item) => item.participantId);
onReorder(participantIds);
onReorder(reorderedQueue.map((item) => item.participantId));
}
};
},
[queue, onReorder]
);
return (
<div className="p-4">
@ -177,24 +184,22 @@ export const QueueSection = memo(function QueueSection({
onDragEnd={handleDragEnd}
>
<SortableContext
items={queue.map((item) => item.id)}
items={queueIds}
strategy={verticalListSortingStrategy}
>
<div className="space-y-1.5 mb-4">
{queue.map((item, index) => {
const participant = availableParticipants.find(
(p) => p.id === item.participantId
);
const participant = participantMap.get(item.participantId);
return (
<SortableQueueItem
key={item.id}
item={item}
index={index}
participantName={participant?.name || "Unknown"}
participantName={participant?.name ?? "Unknown"}
sportName={participant?.sport.name}
canPick={canPick}
onRemove={() => onRemoveFromQueue(item.id)}
onDraft={onMakePick ? () => onMakePick(item.participantId) : undefined}
onRemove={onRemoveFromQueue}
onDraft={onMakePick}
/>
);
})}