fix(git): restore changes panel visibility and sidebar sync (#886)

* fix: restore git changes panel visibility and sidebar sync

Two independent fixes:

1. ChangesSection virtualizer returned empty rows due to useMemo caching
   getVirtualItems() with a stale stable reference. First render produced
   an empty array, and useMemo never recomputed because rowVirtualizer
   reference never changed. Removed the useMemo to call getVirtualItems()
   directly on every render. Also added a ResizeObserver to force
   remeasurement on visibility transitions (defensive).

2. RightSidebarTabs now keeps git status fresh while the sidebar is open
   via useRightSidebarGitSync hook (10s polling with ensureStatus).
   Replaces the GitPollingProvider removed in commit d9821716.

* fix(virtualizer): prevent React error #185 from render-phase getVirtualItems

Restoring useMemo for virtualRows with totalSize as an invalidation
dependency. Calling getVirtualItems() directly during render triggers
the virtualizer's maybeNotify() → onChange() → useReducer dispatch,
causing React error #185 (https://react.dev/errors/185 — cannot
update a component while rendering a different component).

The original useMemo([rowVirtualizer, shouldVirtualize]) was removed
because rowVirtualizer is a stable useState ref that never changes,
leaving the memo permanently stale after the first empty render.
Adding totalSize (from getTotalSize()) as a dependency solves this:
it changes whenever the virtualizer recalculates after measure/scroll,
ensuring getVirtualItems() returns fresh rows while staying wrapped
in useMemo.
This commit is contained in:
jwcrystal
2026-04-11 23:29:35 +03:00
committed by GitHub
parent b91a72c74b
commit 700138c9b5
2 changed files with 61 additions and 3 deletions
@@ -4,13 +4,44 @@ import { RiFolder3Line, RiGitBranchLine } from '@remixicon/react';
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
import { GitView } from '@/components/views';
import { useUIStore } from '@/stores/useUIStore';
import { useGitStore } from '@/stores/useGitStore';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { SidebarFilesTree } from './SidebarFilesTree';
type RightTab = 'git' | 'files';
/**
* Keeps git status fresh while the right sidebar is open.
* Replaces the GitPollingProvider removed in commit b2d5ccb4.
* The previous polling ran globally; now we only refresh when the sidebar is open.
*/
function useRightSidebarGitSync(directory: string | undefined, isSidebarOpen: boolean) {
const { git } = useRuntimeAPIs();
const ensureStatus = useGitStore((state) => state.ensureStatus);
React.useEffect(() => {
if (!directory || !git || !isSidebarOpen) return;
void ensureStatus(directory, git);
const POLL_INTERVAL = 10_000;
const id = setInterval(() => {
if (typeof document !== 'undefined' && document.hidden) return;
void ensureStatus(directory, git);
}, POLL_INTERVAL);
return () => clearInterval(id);
}, [directory, git, isSidebarOpen, ensureStatus]);
}
export const RightSidebarTabs: React.FC = () => {
const rightSidebarTab = useUIStore((state) => state.rightSidebarTab);
const setRightSidebarTab = useUIStore((state) => state.setRightSidebarTab);
const isRightSidebarOpen = useUIStore((state) => state.isRightSidebarOpen);
const directory = useEffectiveDirectory();
useRightSidebarGitSync(directory, isRightSidebarOpen);
const tabItems = React.useMemo(() => [
{
@@ -44,4 +75,4 @@ export const RightSidebarTabs: React.FC = () => {
</div>
</div>
);
};
};
@@ -67,9 +67,36 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
enabled: shouldVirtualize,
});
// Force virtualizer to remeasure when the scroll container transitions
// from display:none (hidden tab via keep-alive) back to visible layout.
// Without this, the virtualizer uses stale zero-height measurements and
// renders no rows until the user scrolls.
React.useEffect(() => {
if (!shouldVirtualize) return;
const el = scrollRef.current;
if (!el) return;
const observer = new ResizeObserver(() => {
rowVirtualizer.measure();
});
observer.observe(el);
return () => observer.disconnect();
}, [shouldVirtualize, rowVirtualizer]);
// Compute virtual rows with useMemo. We include totalSize as a dependency so
// that when the ResizeObserver calls measure() — which clears the itemSizeCache
// and recalculates — the size change invalidates the memo and getVirtualItems()
// returns fresh rows. Using useMemo avoids calling getVirtualItems() directly in
// the render body, which can trigger maybeNotify() → onChange() → useReducer
// dispatch during render (React minified error #185).
const totalSize = rowVirtualizer.getTotalSize();
const virtualRows = React.useMemo(
() => (shouldVirtualize ? rowVirtualizer.getVirtualItems() : []),
[rowVirtualizer, shouldVirtualize],
// totalSize invalidates the memo when the virtualizer recalculates after
// measure/scroll, ensuring getVirtualItems() returns up-to-date rows.
// Without it, the stable rowVirtualizer ref would never invalidate the memo
// and rows would stay empty after measure().
() => (shouldVirtualize && totalSize >= 0 ? rowVirtualizer.getVirtualItems() : []),
[shouldVirtualize, rowVirtualizer, totalSize],
);
React.useEffect(() => {