Move source control selection refs out of effects

This commit is contained in:
Neil
2026-07-10 18:00:27 -07:00
parent 6ed499d0b4
commit ed037e917c
2 changed files with 93 additions and 15 deletions
@@ -0,0 +1,88 @@
// @vitest-environment happy-dom
import { act, Suspense, startTransition, useRef, useState } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { GitStatusEntry } from '../../../../shared/types'
import { useSourceControlSelection, type FlatEntry } from './useSourceControlSelection'
const OLD_ENTRY: FlatEntry = {
key: 'unstaged::old.ts',
area: 'unstaged',
entry: { path: 'old.ts', area: 'unstaged', status: 'modified' }
}
const NEW_ENTRY: FlatEntry = {
key: 'unstaged::new.ts',
area: 'unstaged',
entry: { path: 'new.ts', area: 'unstaged', status: 'modified' }
}
describe('useSourceControlSelection committed event state', () => {
let container: HTMLDivElement
let root: Root
beforeEach(() => {
container = document.createElement('div')
document.body.append(container)
root = createRoot(container)
})
afterEach(async () => {
await act(async () => root.unmount())
container.remove()
})
it('keeps the visible row callback while the next render is suspended', async () => {
const openedOld = vi.fn<(entry: GitStatusEntry) => void>()
const openedNew = vi.fn<(entry: GitStatusEntry) => void>()
const neverResolves = new Promise<never>(() => {})
let showNext!: () => void
function SelectionHarness({ next }: { next: boolean }): React.JSX.Element {
const containerRef = useRef<HTMLDivElement>(null)
const selection = useSourceControlSelection({
flatEntries: next ? [NEW_ENTRY] : [OLD_ENTRY],
onOpenDiff: next ? openedNew : openedOld,
containerRef
})
if (next) {
throw neverResolves
}
return (
<div ref={containerRef}>
<button
onClick={(event) => selection.handleSelect(event, OLD_ENTRY.key, OLD_ENTRY.entry)}
>
Open old row
</button>
</div>
)
}
function App(): React.JSX.Element {
const [next, setNext] = useState(false)
showNext = () => startTransition(() => setNext(true))
return (
<Suspense fallback={<div>Loading next worktree</div>}>
<SelectionHarness next={next} />
</Suspense>
)
}
await act(async () => root.render(<App />))
await act(async () => {
showNext()
await Promise.resolve()
})
const visibleButton = container.querySelector('button')
expect(visibleButton?.textContent).toBe('Open old row')
visibleButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
expect(openedOld).toHaveBeenCalledWith(OLD_ENTRY.entry)
expect(openedNew).not.toHaveBeenCalled()
})
})
@@ -1,4 +1,4 @@
import { useState, useCallback, useEffect, useRef, type RefObject } from 'react'
import { useState, useCallback, useEffect, useLayoutEffect, useRef, type RefObject } from 'react'
import type { GitStatusEntry } from '../../../../shared/types'
import type { SourceControlRowOpenEvent } from './source-control-split-open'
@@ -91,25 +91,15 @@ export function useSourceControlSelection({
const onOpenDiffRef = useRef(onOpenDiff)
const shouldOpenAsSplitRef = useRef(shouldOpenAsSplit)
useEffect(() => {
useLayoutEffect(() => {
// Why: stable row handlers must read one committed selection/worktree
// snapshot; render-time writes can leak values from an interrupted render.
flatEntriesRef.current = flatEntries
}, [flatEntries])
useEffect(() => {
anchorKeyRef.current = anchorKey
}, [anchorKey])
useEffect(() => {
selectedKeysRef.current = selectedKeys
}, [selectedKeys])
useEffect(() => {
onOpenDiffRef.current = onOpenDiff
}, [onOpenDiff])
useEffect(() => {
shouldOpenAsSplitRef.current = shouldOpenAsSplit
}, [shouldOpenAsSplit])
}, [flatEntries, anchorKey, selectedKeys, onOpenDiff, shouldOpenAsSplit])
const reconciledSelection = reconcileSourceControlSelectionState({
selectedKeys,