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