From f0bbc826846574acf6f752768622b960289fb86e Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sun, 16 Aug 2026 16:42:21 -0700 Subject: [PATCH] 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. --- .../pull-request-page/files/active-section.ts | 12 ++++++++++ .../files/combined-diff-viewer.tsx | 21 +++++++++-------- .../files/inline-comments.ts | 2 +- .../files/section-heights.ts | 23 +++++++++++++++++++ 4 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 src/renderer/src/components/pull-request-page/files/active-section.ts create mode 100644 src/renderer/src/components/pull-request-page/files/section-heights.ts diff --git a/src/renderer/src/components/pull-request-page/files/active-section.ts b/src/renderer/src/components/pull-request-page/files/active-section.ts new file mode 100644 index 00000000000..cc234d7a012 --- /dev/null +++ b/src/renderer/src/components/pull-request-page/files/active-section.ts @@ -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] +} diff --git a/src/renderer/src/components/pull-request-page/files/combined-diff-viewer.tsx b/src/renderer/src/components/pull-request-page/files/combined-diff-viewer.tsx index 8a50aabcf26..3cee0fee2aa 100644 --- a/src/renderer/src/components/pull-request-page/files/combined-diff-viewer.tsx +++ b/src/renderer/src/components/pull-request-page/files/combined-diff-viewer.tsx @@ -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([]) const [sideBySide, setSideBySide] = useState(false) const [fileTreeCollapsed, setFileTreeCollapsed] = useState(false) - const [sectionHeights, setSectionHeights] = useState>({}) - const [activeTreeSectionKey, setActiveTreeSectionKey] = useState(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(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} diff --git a/src/renderer/src/components/pull-request-page/files/inline-comments.ts b/src/renderer/src/components/pull-request-page/files/inline-comments.ts index f2370f05296..a23189f25a9 100644 --- a/src/renderer/src/components/pull-request-page/files/inline-comments.ts +++ b/src/renderer/src/components/pull-request-page/files/inline-comments.ts @@ -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' diff --git a/src/renderer/src/components/pull-request-page/files/section-heights.ts b/src/renderer/src/components/pull-request-page/files/section-heights.ts new file mode 100644 index 00000000000..0caba64fb80 --- /dev/null +++ b/src/renderer/src/components/pull-request-page/files/section-heights.ts @@ -0,0 +1,23 @@ +import { useCallback, useState, type SetStateAction } from 'react' + +const EMPTY_SECTION_HEIGHTS: Record = {} + +export function usePRFileSectionHeights( + entrySignature: string +): [Record, (updater: SetStateAction>) => 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>) => { + 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] +}