perf(sidebar): stable per-group onLineageToggle identity so lineage-parent WorktreeCards memo-bail

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-20 18:57:07 -07:00
co-authored by Orca
parent afb82bc336
commit c210a44705
3 changed files with 82 additions and 5 deletions
@@ -24,6 +24,7 @@ import {
Trash2
} from 'lucide-react'
import { useAppStore } from '@/store'
import { createLineageToggleHandlerCache } from './worktree-lineage-toggle-handler-cache'
import { useShallow } from 'zustand/react/shallow'
import type { AppState } from '@/store/types'
import {
@@ -2449,6 +2450,12 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
},
[recordCurrentScrollAnchor, toggleGroup]
)
// Why: memo'd WorktreeCard needs a per-group-key stable onLineageToggle
// identity to bail out of re-renders; see worktree-lineage-toggle-handler-cache.
const getLineageToggleHandler = useMemo(
() => createLineageToggleHandlerCache(toggleGroupWithScrollAnchor),
[toggleGroupWithScrollAnchor]
)
const navigateWorktree = useCallback(
(direction: 'up' | 'down') => {
@@ -4856,11 +4863,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
lineageChildrenStyle={lineageChildrenStyle}
onLineageToggle={
lineageToggleGroupKey
? (event) => {
event.preventDefault()
event.stopPropagation()
toggleGroupWithScrollAnchor(lineageToggleGroupKey)
}
? getLineageToggleHandler(lineageToggleGroupKey)
: undefined
}
/>
@@ -0,0 +1,48 @@
import { describe, expect, it, vi } from 'vitest'
import type React from 'react'
import { createLineageToggleHandlerCache } from './worktree-lineage-toggle-handler-cache'
const makeEvent = (): React.MouseEvent<HTMLButtonElement> =>
({
preventDefault: vi.fn(),
stopPropagation: vi.fn()
}) as unknown as React.MouseEvent<HTMLButtonElement>
describe('createLineageToggleHandlerCache', () => {
it('returns a referentially stable handler for the same group key across calls', () => {
const getHandler = createLineageToggleHandlerCache(vi.fn())
const first = getHandler('lineage:alpha')
const second = getHandler('lineage:alpha')
// Why: stable identity is what lets React.memo'd WorktreeCard bail out.
expect(second).toBe(first)
})
it('returns distinct handlers for distinct group keys', () => {
const getHandler = createLineageToggleHandlerCache(vi.fn())
expect(getHandler('lineage:alpha')).not.toBe(getHandler('lineage:beta'))
})
it('prevents default, stops propagation, and toggles the bound group key', () => {
const toggleGroup = vi.fn()
const getHandler = createLineageToggleHandlerCache(toggleGroup)
const event = makeEvent()
getHandler('lineage:alpha')(event)
expect(event.preventDefault).toHaveBeenCalledTimes(1)
expect(event.stopPropagation).toHaveBeenCalledTimes(1)
expect(toggleGroup).toHaveBeenCalledExactlyOnceWith('lineage:alpha')
})
it('keeps each cached handler bound to its own group key', () => {
const toggleGroup = vi.fn()
const getHandler = createLineageToggleHandlerCache(toggleGroup)
getHandler('lineage:beta')(makeEvent())
expect(toggleGroup).toHaveBeenCalledExactlyOnceWith('lineage:beta')
})
})
@@ -0,0 +1,26 @@
import type React from 'react'
export type LineageToggleHandler = (event: React.MouseEvent<HTMLButtonElement>) => void
// Why: WorktreeCard is React.memo'd; an inline arrow per row would give
// onLineageToggle a fresh identity every render and defeat the memo bail-out
// for every lineage-parent card on each sort/status epoch bump. Cache one
// handler per group key (bounded by lineage-group count) so identity is stable.
export const createLineageToggleHandlerCache = (
toggleGroup: (groupKey: string) => void
): ((groupKey: string) => LineageToggleHandler) => {
const handlersByGroupKey = new Map<string, LineageToggleHandler>()
return (groupKey: string) => {
const cached = handlersByGroupKey.get(groupKey)
if (cached) {
return cached
}
const handler: LineageToggleHandler = (event) => {
event.preventDefault()
event.stopPropagation()
toggleGroup(groupKey)
}
handlersByGroupKey.set(groupKey, handler)
return handler
}
}