Extract PR file state into entry-scoped hooks

Replace manual state resets with custom hooks that automatically clear section heights and active section when switching PR entries. This prevents state leakage between files and simplifies the diff viewer component. Also validates the active section key exists before passing it to child components.
This commit is contained in:
Jinjing
2026-08-16 19:21:36 -07:00
parent 6dfef2b2df
commit f0bbc82684
4 changed files with 48 additions and 10 deletions
@@ -0,0 +1,12 @@
import { useCallback, useState } from 'react'
export function usePRFileActiveSection(
entrySignature: string
): [string | null, (key: string | null) => void] {
const [state, setState] = useState({ entrySignature, key: null as string | null })
const setActiveSection = useCallback(
(key: string | null) => setState({ entrySignature, key }),
[entrySignature]
)
return [state.entrySignature === entrySignature ? state.key : null, setActiveSection]
}
@@ -1,4 +1,3 @@
/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: switching PRs re-seeds the whole diff view (sections, heights, scroll target, cached loaded set) from the persisted cache, which cannot be derived during render. */
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
import type { editor as monacoEditor } from 'monaco-editor'
@@ -34,6 +33,8 @@ import { addPullRequestLineComment } from './line-comment'
import { usePRFileSectionLoader } from './section-loader'
import { usePRFilesDiffViewPersistence } from './view-restore'
import { buildInlineReviewComments } from './inline-comments'
import { usePRFileSectionHeights } from './section-heights'
import { usePRFileActiveSection } from './active-section'
export function PRFilesCombinedDiffViewer({
files,
@@ -106,8 +107,8 @@ export function PRFilesCombinedDiffViewer({
const [sections, setSections] = useState<DiffSection[]>([])
const [sideBySide, setSideBySide] = useState(false)
const [fileTreeCollapsed, setFileTreeCollapsed] = useState(false)
const [sectionHeights, setSectionHeights] = useState<Record<number, number>>({})
const [activeTreeSectionKey, setActiveTreeSectionKey] = useState<string | null>(null)
const [sectionHeights, setSectionHeights] = usePRFileSectionHeights(entrySignature)
const [activeTreeSectionKey, setActiveTreeSectionKey] = usePRFileActiveSection(entrySignature)
// Why: short virtualizer-key token; entrySignature serializes every file and would be copied into every item key.
const [entryRevision, setEntryRevision] = useState(0)
const scrollContainerRef = useRef<HTMLDivElement>(null)
@@ -147,8 +148,6 @@ export function PRFilesCombinedDiffViewer({
loadedIndicesRef.current.clear()
loadingIndicesRef.current.clear()
pendingRestoreScrollTopRef.current = prFilesDiffScrollTopCache.get(viewStateKey) ?? null
setSectionHeights({})
setActiveTreeSectionKey(null)
setSections(
entries.map((entry) => ({
key: getPRFileSectionKey(entry.path),
@@ -167,7 +166,7 @@ export function PRFilesCombinedDiffViewer({
largeDiffRenderLimit: null
}))
)
}, [entries, entrySignature, viewStateKey])
}, [entries, entrySignature, setActiveTreeSectionKey, setSectionHeights, viewStateKey])
const { loadSection, retrySection, toggleSection, setAllSectionsCollapsed } =
usePRFileSectionLoader({
@@ -189,6 +188,10 @@ export function PRFilesCombinedDiffViewer({
const allSectionsCollapsed = sections.length > 0 && sections.every((section) => section.collapsed)
const sectionIndexByKey = useMemo(() => createCombinedDiffSectionIndexMap(sections), [sections])
const visibleActiveTreeSectionKey =
activeTreeSectionKey && sectionIndexByKey.has(activeTreeSectionKey)
? activeTreeSectionKey
: null
const viewedSectionKeys = useMemo(
() => new Set(files.filter(isPRFileViewed).map((file) => getPRFileSectionKey(file.path))),
[files]
@@ -237,7 +240,7 @@ export function PRFilesCombinedDiffViewer({
sectionHeights,
sideBySide,
fileTreeCollapsed,
activeTreeSectionKey,
activeTreeSectionKey: visibleActiveTreeSectionKey,
loadedIndicesRef,
scrollContainerRef,
pendingRestoreScrollTopRef
@@ -257,7 +260,7 @@ export function PRFilesCombinedDiffViewer({
setActiveTreeSectionKey(sectionsRef.current[navigatedIndex]?.key ?? null)
}
},
[sectionIndexByKey, toggleSection, virtualizer]
[sectionIndexByKey, setActiveTreeSectionKey, toggleSection, virtualizer]
)
const openFilesOnGitHub = useCallback(() => {
@@ -334,7 +337,7 @@ export function PRFilesCombinedDiffViewer({
worktreePath={repoPath}
entries={entries}
sectionIndexByKey={sectionIndexByKey}
activeSectionKey={activeTreeSectionKey}
activeSectionKey={visibleActiveTreeSectionKey}
viewedSectionKeys={viewedSectionKeys}
collapsed={fileTreeCollapsed}
onCollapsedChange={setFileTreeCollapsed}
@@ -1,4 +1,4 @@
import type { DecoratedDiffComment } from '@/components/diff-comments/useDiffCommentDecorator'
import type { DecoratedDiffComment } from '@/components/diff-comments/decorated-diff-comment'
import { formatRelativeTime } from '@/components/github/work-item-state-presentation'
import type { PRComment } from '../../../../../shared/github/comment-types'
@@ -0,0 +1,23 @@
import { useCallback, useState, type SetStateAction } from 'react'
const EMPTY_SECTION_HEIGHTS: Record<number, number> = {}
export function usePRFileSectionHeights(
entrySignature: string
): [Record<number, number>, (updater: SetStateAction<Record<number, number>>) => void] {
const [state, setState] = useState(() => ({ entrySignature, values: EMPTY_SECTION_HEIGHTS }))
const heights = state.entrySignature === entrySignature ? state.values : EMPTY_SECTION_HEIGHTS
const setHeights = useCallback(
(updater: SetStateAction<Record<number, number>>) => {
setState((prev) => {
const current = prev.entrySignature === entrySignature ? prev.values : EMPTY_SECTION_HEIGHTS
return {
entrySignature,
values: typeof updater === 'function' ? updater(current) : updater
}
})
},
[entrySignature]
)
return [heights, setHeights]
}