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) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 21:49:32 +02:00
parent b9916866b3
commit 96624bf853

View File

@@ -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 (
<div className="flex h-full items-center justify-center text-text-muted">
@@ -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 (
<div ref={scrollRef} className="h-full overflow-auto bg-bg p-4 pb-20">
<div className="mb-4 flex items-center gap-2 text-xs text-text-muted">