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:
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user