fix(renderer): document intentional render-time refs

This commit is contained in:
Neil
2026-08-31 04:07:58 -07:00
parent ea8e6e595a
commit 6fc78dd869
10 changed files with 30 additions and 4 deletions
@@ -138,6 +138,8 @@ export function useAutomationsPageLocalState(store: AutomationsPageStoreState) {
scheduleWarning: null
})
const draftRef = useRef(draft)
// Keep async editor actions on the latest draft before they can run.
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
draftRef.current = draft
return {
@@ -54,6 +54,8 @@ export function useFloatingTerminalPanelLocalState() {
)
const doubleTapDetectorRef = useRef<ModifierDoubleTapDetector | null>(null)
if (!doubleTapDetectorRef.current) {
// The detector must exist before event handlers are published.
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
doubleTapDetectorRef.current = new ModifierDoubleTapDetector()
}
const shortcutFocusFrameRef = useRef<number | null>(null)
@@ -89,12 +89,15 @@ export function useWorkspaceSpaceManagerProjection(args: {
// Why: these ids are local UI state derived from the latest scan rows. Repair
// them before commit so stale selections cannot flash after a scan changes.
if (inspectedWorktreeId !== nextInspectedWorktreeId) {
// react-doctor-disable-next-line react-doctor/no-prop-callback-in-render
setInspectedWorktreeId(nextInspectedWorktreeId)
}
if (nextSelectedIds !== selectedIds) {
// react-doctor-disable-next-line react-doctor/no-prop-callback-in-render
setSelectedIds(nextSelectedIds)
}
if (treemapZoomWorktreeId !== nextTreemapZoomWorktreeId) {
// react-doctor-disable-next-line react-doctor/no-prop-callback-in-render
setTreemapZoomWorktreeId(nextTreemapZoomWorktreeId)
}
@@ -57,9 +57,12 @@ export function useTerminalPaneFoundation(
const panePtyBindingsRef = useRef<Map<number, IDisposable>>(new Map())
const replayingPanesRef = useRef<Map<number, number>>(new Map())
const isActiveRef = useRef(isActive)
// Event/PTY callbacks need current visibility before passive effects run.
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
isActiveRef.current = isActive
const isRendererVisible = isVisible && isWorktreeActive
const isVisibleRef = useRef(isRendererVisible)
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
isVisibleRef.current = isRendererVisible
const {
nativeChatTranscriptIsLocalReadable,
@@ -91,6 +94,8 @@ export function useTerminalPaneFoundation(
}, [])
const [searchOpen, setSearchOpen] = useState(false)
const searchOpenRef = useRef(false)
// Keyboard callbacks must observe current search state immediately.
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
searchOpenRef.current = searchOpen
const searchStateRef = useRef<SearchState>({ query: '', caseSensitive: false, regex: false })
const [pendingCloseConfirmation, setPendingCloseConfirmation] = useState<{
@@ -158,6 +158,8 @@ export function useTerminalPaneStartupActions(controller: TerminalPaneStoreContr
}, [issueCommandSplit, tabId, consumeTabIssueCommandSplit])
const settingsRef = useRef(settings)
// Startup callbacks can run before the next effect commit.
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
settingsRef.current = settings
const openLinksInAppPreferencePromiseRef = useRef<Promise<boolean> | null>(null)
const requestOpenLinksInAppPreference = useCallback(
@@ -192,8 +194,12 @@ export function useTerminalPaneStartupActions(controller: TerminalPaneStoreContr
)
const effectiveMacOptionAsAlt = useEffectiveMacOptionAsAlt(settings?.terminalMacOptionAsAlt)
const macOptionAsAltRef = useRef<MacOptionAsAlt>(effectiveMacOptionAsAlt)
// Keyboard listeners need the current preference without a render lag.
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
macOptionAsAltRef.current = effectiveMacOptionAsAlt
const onPtyExitRef = useRef(onPtyExit)
// PTY teardown may call this ref before passive effects flush.
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
onPtyExitRef.current = onPtyExit
const systemPrefersDark = useSystemPrefersDark()
const dispatchNotification = useNotificationDispatch(worktreeId)
@@ -20,6 +20,8 @@ export function useTerminalPaneTitleState(controller: TerminalPaneFoundation) {
} = controller
const [paneTitles, setPaneTitles] = useState<Record<number, string>>({})
const paneTitlesRef = useRef<Record<number, string>>({})
// Rename handlers read this ref synchronously during the render that changes it.
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
paneTitlesRef.current = paneTitles
const removedTitleLeafIdsRef = useRef<Set<string>>(new Set())
const clearedScrollbackLeafIdsRef = useRef<Set<string>>(new Set())
@@ -89,7 +89,10 @@ export function useTaskPageGitHubListState(model: TaskPageProviderStateModel) {
const [currentPage, setCurrentPage] = useState(0)
const pagesRef = useRef(pages)
const currentPageRef = useRef(currentPage)
// Fetch callbacks need the latest paging window immediately after render.
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
pagesRef.current = pages
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
currentPageRef.current = currentPage
const githubResumeConsumedRef = useRef(false)
const githubResumeContextRef = useRef('')
@@ -17,6 +17,8 @@ export function useTaskPageGitHubQuietRefreshPrelude(model: TaskPageGitHubLandin
queryKey: githubWorkItemMutationQueryKey,
generation: 0
})
// Revalidation may run before a passive effect observes a new key.
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
quietRevalidateScopeRef.current = advanceTaskPageQuietRevalidateScope(
quietRevalidateScopeRef.current,
githubWorkItemMutationQueryKey
@@ -71,6 +71,8 @@ export function useTaskPageStoreBindings() {
const checkJiraConnection = useAppStore((s) => s.checkJiraConnection)
const providerRuntimeContextKey = getProviderRuntimeContextKey(settings)
const providerRuntimeContextKeyRef = useRef(providerRuntimeContextKey)
// Submit handlers must fence against the current provider context immediately.
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
providerRuntimeContextKeyRef.current = providerRuntimeContextKey
const linearStatusCurrent = linearStatusContextKey === providerRuntimeContextKey
const jiraStatusCurrent = jiraStatusContextKey === providerRuntimeContextKey
@@ -1,9 +1,6 @@
import { useDeferredValue, useMemo, useRef, useState } from 'react'
import type { WorktreePaletteRequestGuard } from '@/lib/worktree-palette-create-action'
import {
EMPTY_PALETTE_FILTER,
type PaletteFilterState
} from '@/components/cmd-j/palette-filter'
import { EMPTY_PALETTE_FILTER, type PaletteFilterState } from '@/components/cmd-j/palette-filter'
import { parseCmdJTaskSourceUrl } from '@/lib/worktree-palette-task-url-match'
import { getWorktreePaletteCreateActionState } from '@/lib/worktree-palette-create-action'
import type { CmdJActiveGroupSnapshot } from '@/components/cmd-j/quick-action-context'
@@ -20,6 +17,8 @@ export function useWorktreeJumpPaletteLocalState({
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const liveQueryRef = useRef(query)
// Keyboard handlers must see the current query before effects flush.
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
liveQueryRef.current = query
const taskSourceUrl = useMemo(() => parseCmdJTaskSourceUrl(query), [query])
const paletteSearchQuery = taskSourceUrl ? query.trim() : deferredQuery.trim()