From 96624bf853eee4828f96f985de11ded61effafee Mon Sep 17 00:00:00 2001 From: dtoro Date: Tue, 21 Apr 2026 21:49:32 +0200 Subject: [PATCH] fix(duplicates): hoist memo hooks above early returns Rules-of-hooks violation: useRef and three useCallbacks sat after the isLoading/isError/empty early-return block, so first render (loading) called N hooks and the post-data render called N+4, crashing the view. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/duplicates/DuplicatesView.tsx | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/duplicates/DuplicatesView.tsx b/frontend/src/components/duplicates/DuplicatesView.tsx index 76455bc..b9fc5aa 100644 --- a/frontend/src/components/duplicates/DuplicatesView.tsx +++ b/frontend/src/components/duplicates/DuplicatesView.tsx @@ -180,6 +180,26 @@ export function DuplicatesView() { return () => window.removeEventListener('keydown', onKeyDown) }, [allMemberIds, activePhotoId, columns, selectPhoto, virtualizer]) + // Stable handlers — passed through memoised section + thumbnail so + // React.memo is actually effective. The `discardMutation.mutate` and + // store actions have stable identity already; we wrap them once so the + // closure identity doesn't change per render. Must sit above the early + // returns below — rules of hooks forbid conditional hook calls. + const allMemberIdsRef = useRef(allMemberIds) + allMemberIdsRef.current = allMemberIds + const handleKeepBest = useCallback( + (discardIds: string[]) => discardMutation.mutate(discardIds), + [discardMutation] + ) + const handlePreviewMember = useCallback( + (memberId: string) => openPreview(memberId, allMemberIdsRef.current), + [openPreview] + ) + const handleSelectMember = useCallback( + (memberId: string) => selectPhoto(memberId), + [selectPhoto] + ) + if (isLoading) { return (
@@ -211,25 +231,6 @@ export function DuplicatesView() { ) } - // Stable handlers — passed through memoised section + thumbnail so - // React.memo is actually effective. The `discardMutation.mutate` and - // store actions have stable identity already; we wrap them once so the - // closure identity doesn't change per render. - const allMemberIdsRef = useRef(allMemberIds) - allMemberIdsRef.current = allMemberIds - const handleKeepBest = useCallback( - (discardIds: string[]) => discardMutation.mutate(discardIds), - [discardMutation] - ) - const handlePreviewMember = useCallback( - (memberId: string) => openPreview(memberId, allMemberIdsRef.current), - [openPreview] - ) - const handleSelectMember = useCallback( - (memberId: string) => selectPhoto(memberId), - [selectPhoto] - ) - return (