Context
StatusBar reads the entire tabs array and activeTabId from useEditorStore. Both reads can be narrowed to selectors.
Problem
src/components/layout/StatusBar.tsx:56-58:
const tabs = useEditorStore((s) => s.tabs);
const activeTabId = useEditorStore((s) => s.activeTabId);
The two reads are used for:
dirtyTabs = tabs.filter(t => t.isDirty) and hasDirtyTabs = dirtyTabs.length > 0 (lines 96-97) — used only by the update-available button's dirty-tab guard.
activeTab = tabs.find(t => t.id === activeTabId) (line 105) — used only for selectedDatabase = activeTab?.database ?? activeConn?.database (line 106).
Both depend on a small subset of the tab state. Yet every updateTabContent keystroke writes tabs: state.tabs.map(...) (src/stores/editorStore.ts:313-316) and triggers a StatusBar re-render. With Monaco auto-save debounce at 500ms (editorStore.ts:355-365), this isn't a per-keystroke storm, but every editor action (rename, reorder, setDirty, setConnection) re-renders StatusBar. The 404-LOC component re-rendering on tab churn makes its own update-overlay reflow visible.
The cross-cutting audit (docs/audits/cross-cutting.md F13) flagged this as P1.
Files
- src/components/layout/StatusBar.tsx:56-58 — wide selectors
- src/stores/editorStore.ts:313-316 —
updateTabContent triggers map and re-render
Repro
// In a test, mount StatusBar, count renders.
useEditorStore.setState({ tabs: [...prevTabs, newTab] }); // unrelated new tab
// StatusBar re-renders even if activeTab.database didn't change.
Expected
StatusBar only re-renders when:
hasDirtyTabs changes.
activeTab.database changes.
- The result/error state changes (already uses narrow selectors).
Proposed fix
Scope S. Use narrow selectors:
const hasDirtyTabs = useEditorStore((s) => s.tabs.some((t) => t.isDirty));
const selectedDatabase = useEditorStore((s) => {
const tab = s.tabs.find((t) => t.id === s.activeTabId);
return tab?.database;
});
Or split into <DirtyTabIndicator /> and <SelectedDatabaseIndicator /> sub-components (per #451 / F6) so each subscribes narrowly.
Acceptance
A test using useEditorStore.setState({ tabs: [...] }) asserts StatusBar does not re-render if no tab's isDirty changed and the active tab's database didn't change.
Needs human verify
No (selector refactor).
Labels: audit, area/cross-cutting, severity/p1, kind/arch
Context
StatusBarreads the entiretabsarray andactiveTabIdfromuseEditorStore. Both reads can be narrowed to selectors.Problem
src/components/layout/StatusBar.tsx:56-58:The two reads are used for:
dirtyTabs = tabs.filter(t => t.isDirty)andhasDirtyTabs = dirtyTabs.length > 0(lines 96-97) — used only by the update-available button's dirty-tab guard.activeTab = tabs.find(t => t.id === activeTabId)(line 105) — used only forselectedDatabase = activeTab?.database ?? activeConn?.database(line 106).Both depend on a small subset of the tab state. Yet every
updateTabContentkeystroke writestabs: state.tabs.map(...)(src/stores/editorStore.ts:313-316) and triggers a StatusBar re-render. With Monaco auto-save debounce at 500ms (editorStore.ts:355-365), this isn't a per-keystroke storm, but every editor action (rename, reorder, setDirty, setConnection) re-renders StatusBar. The 404-LOC component re-rendering on tab churn makes its own update-overlay reflow visible.The cross-cutting audit (docs/audits/cross-cutting.md F13) flagged this as P1.
Files
updateTabContenttriggers map and re-renderRepro
Expected
StatusBaronly re-renders when:hasDirtyTabschanges.activeTab.databasechanges.Proposed fix
Scope S. Use narrow selectors:
Or split into
<DirtyTabIndicator />and<SelectedDatabaseIndicator />sub-components (per #451 / F6) so each subscribes narrowly.Acceptance
A test using
useEditorStore.setState({ tabs: [...] })asserts StatusBar does not re-render if no tab'sisDirtychanged and the active tab'sdatabasedidn't change.Needs human verify
No (selector refactor).
Labels: audit, area/cross-cutting, severity/p1, kind/arch