Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions interface/src/components/TopBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createContext, useContext, useRef, useCallback, useSyncExternalStore, type ReactNode, type MouseEvent } from "react";
import { createContext, useContext, useRef, useEffect, useCallback, useSyncExternalStore, type ReactNode, type MouseEvent } from "react";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n interface/src/components/TopBar.tsx | head -80

Repository: spacedriveapp/spacebot

Length of output: 2841


🏁 Script executed:

rg "useSetTopBar" -A 3 -B 1

Repository: spacedriveapp/spacebot

Length of output: 3773


🏁 Script executed:

sed -n '1,100p' interface/src/routes/Overview.tsx | tail -60

Repository: spacedriveapp/spacebot

Length of output: 2116


🏁 Script executed:

sed -n '40,100p' interface/src/routes/Overview.tsx

Repository: spacedriveapp/spacebot

Length of output: 2158


🏁 Script executed:

sed -n '1,50p' interface/src/routes/Overview.tsx

Repository: spacedriveapp/spacebot

Length of output: 1602


🏁 Script executed:

cat -n interface/src/components/TopBar.tsx | sed -n '8,35p'

Repository: spacedriveapp/spacebot

Length of output: 831


🏁 Script executed:

cat > /tmp/verify_hooks.md << 'EOF'
# React Hook Behavior Verification

## Current Code (useEffect, no deps)
```javascript
useEffect(() => {
  store.setContent(nodeRef.current);
  return () => store.setContent(null);
});
```
- Effect runs after EVERY render
- Cleanup runs before next effect AND on unmount
- Result: store.setContent(null) executes on every rerender

## Proposed Code (useLayoutEffect, with deps)
```javascript
useLayoutEffect(() => {
  store.setContent(node);
}, [store, node]);

useLayoutEffect(() => {
  return () => store.setContent(null);
}, [store]);
```
- First effect: runs when store or node changes, BEFORE paint
- Second effect: cleanup runs when store changes or on unmount
- Since store is stable, cleanup only runs on unmount
- Result: store.setContent(null) only on unmount, not on every rerender
EOF
cat /tmp/verify_hooks.md

Repository: spacedriveapp/spacebot

Length of output: 860


Move topbar writes to layout effects with explicit dependencies to eliminate flickering and excessive store updates.

The no-deps useEffect runs after every render, including its cleanup that calls store.setContent(null) before each subsequent setup. With polling queries like Overview's 5–10 second refetch intervals and inline JSX passed to useSetTopBar, this causes repeated null → content cycles and stale content paints (since passive effects run after paint). Use useLayoutEffect with [store, node] for the content write (runs before paint) and a separate cleanup effect with [store] dependency (cleanup only on unmount), eliminating the per-rerender null resets and render-phase lag.

🛠️ Suggested fix
-import { createContext, useContext, useRef, useEffect, useCallback, useSyncExternalStore, type ReactNode, type MouseEvent } from "react";
+import { createContext, useContext, useRef, useLayoutEffect, useCallback, useSyncExternalStore, type ReactNode, type MouseEvent } from "react";
@@
 export function useSetTopBar(node: ReactNode) {
 	const store = useContext(TopBarContext);
 	if (!store) throw new Error("useSetTopBar must be used within TopBarProvider");
 
-	const nodeRef = useRef(node);
-	nodeRef.current = node;
-
-	useEffect(() => {
-		store.setContent(nodeRef.current);
-		return () => store.setContent(null);
-	});
+	useLayoutEffect(() => {
+		store.setContent(node);
+	}, [store, node]);
+
+	useLayoutEffect(() => {
+		return () => store.setContent(null);
+	}, [store]);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/components/TopBar.tsx` at line 1, The topbar write logic
currently uses a no-deps useEffect causing cleanup to run every render
(store.setContent(null)) and producing flicker; change the effect that writes
content to useLayoutEffect with dependencies [store, node] so it runs before
paint and only when those change, and move the cleanup to a separate effect with
dependency [store] so store.setContent(null) runs only on unmount. Locate the
TopBar/useSetTopBar code paths and replace the single no-deps useEffect that
references store.setContent and node with two effects: a useLayoutEffect([store,
node]) to set content and a cleanup-only useEffect([store]) that clears content
on unmount.

import { Link } from "@tanstack/react-router";
import { BASE_PATH } from "@/api/client";
import { IS_TAURI, startDragging } from "@/platform";
Expand Down Expand Up @@ -59,9 +59,13 @@ export function useSetTopBar(node: ReactNode) {
const store = useContext(TopBarContext);
if (!store) throw new Error("useSetTopBar must be used within TopBarProvider");

// Update content synchronously during render (like useSyncExternalStore pattern).
// This avoids the useEffect loop entirely.
store.setContent(node);
const nodeRef = useRef(node);
nodeRef.current = node;

useEffect(() => {
store.setContent(nodeRef.current);
return () => store.setContent(null);
});
}

// ── Component ────────────────────────────────────────────────────────────
Expand Down
Loading