Skip to content

Commit bf7e180

Browse files
authored
Merge pull request #74 from VariableThe/fix/graph-data-undefined
fix(graph): prevent e.graphData crash on unmount when navigating to notes
2 parents 0a49515 + 22316de commit bf7e180

3 files changed

Lines changed: 43 additions & 14 deletions

File tree

AUDIT_LOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
This log tracks all significant changes, updates, and versions in the PaperCache project.
44

5+
## 2026-06-27 (Graph View Bugfix)
6+
**Change:** fix(graph): prevent `e.graphData is not a function` crash on unmount and replace setInterval
7+
8+
**Details/Why:**
9+
1. When navigating to a note from Graph View (`onNodeClick`), the unmounting sequence destroyed internal `react-force-graph-3d` methods on `fgRef.current` before `GraphView`'s effect cleanup ran. Calling `fg.graphData()` threw a TypeError (`e.graphData is not a function`). Added defensive `typeof fg.graphData === 'function'` verification before invocation and introduced `graphDataRef` as a safe fallback cache for node positions.
10+
2. Replaced the active `setInterval` loop in `GraphView.tsx` with a chained `setTimeout` pattern to comply with project timer guidelines (`No setInterval in renderer or main process`).
11+
12+
**Files changed:** `src/GraphView.tsx`, `CHANGELOG.md`, `AUDIT_LOG.md`.
13+
14+
---
15+
516
## 2026-06-27 (v0.5.5 Release & Smart Onboarding)
617
**Change:** chore(release): bump version to 0.5.5; implement smart onboarding and release notes routing
718

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- **Settings Bug Report & About Menu**: Added a "Submit a Bug Report" button under System settings linking directly to the GitHub issue creation form. Added a dedicated "About" section displaying the app logo, current version number, update checker, Ko-fi support link, and a thank you message.
1313

1414
### Fixed
15+
- **Graph View crash on navigation**: Fixed `e.graphData is not a function` TypeError when navigating to a note from the Graph View by adding defensive checks before invoking ref methods on component unmount and falling back to a ref cache. Also replaced `setInterval` with a chained `setTimeout` loop.
1516
- **Windows Onboarding File Linking & Generation**: Fixed a bug on Windows where backslashes in generated note IDs caused internal `/file` links in `Welcome.md` to fail and create duplicate empty notes. Normalized note ID generation across Rust and TypeScript to consistently use forward slashes on all platforms, and ensured onboarding template files regenerate correctly on application updates.
1617
- **Window position/size now persists across restarts**: The window-state plugin's `on_window_ready` fires before the macOS display server is ready, causing `available_monitors()` to return empty and the saved position to be silently discarded. Fixed by deferring window-state restoration via a background thread + `run_on_main_thread` 300ms after `setup()` completes, bypassing the plugin's monitor-intersection check with a direct file read. Both the tray "Quit" and Settings "Quit" buttons now explicitly save window state before exit.
1718
- **Launch at Startup now registers as a proper Login Item**: Changed `MacosLauncher` from `LaunchAgent` to `AppleScript`, which registers PaperCache in System Settings > General > Login Items instead of creating a hidden `launchd` plist. Users can now see and manage the autostart entry directly from System Settings.

src/GraphView.tsx

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,15 @@ export default function GraphView({
6565
const fgRef = useRef<ForceGraphMethods<GraphNode, GraphLink> | undefined>(undefined)
6666

6767
const draggedNodesRef = useRef<Set<string>>(new Set())
68+
const graphDataRef = useRef<{ nodes: GraphNode[]; links: GraphLink[] }>({ nodes: [], links: [] })
6869

6970
useEffect(() => {
7071
let raf: number
7172
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7273
let ctrls: any = null
7374
const setup = () => {
7475
const fg = fgRef.current
75-
if (!fg) {
76+
if (!fg || typeof fg.controls !== 'function') {
7677
raf = requestAnimationFrame(setup)
7778
return
7879
}
@@ -89,8 +90,12 @@ export default function GraphView({
8990
ctrls.zoomSpeed = 6
9091
ctrls.panSpeed = 0.15
9192
ctrls.update()
92-
fg.cameraPosition({ x: 0, y: 0, z: 500 })
93-
setTimeout(() => fg.zoomToFit(400, 50), 300)
93+
if (typeof fg.cameraPosition === 'function') {
94+
fg.cameraPosition({ x: 0, y: 0, z: 500 })
95+
}
96+
setTimeout(() => {
97+
if (fg && typeof fg.zoomToFit === 'function') fg.zoomToFit(400, 50)
98+
}, 300)
9499
}
95100
raf = requestAnimationFrame(setup)
96101
return () => cancelAnimationFrame(raf)
@@ -101,8 +106,7 @@ export default function GraphView({
101106
// (avoids the react-hooks/exhaustive-deps stale-ref warning)
102107
const fg = fgRef.current
103108
return () => {
104-
if (!fg) return
105-
const data = fg.graphData()
109+
const data = fg && typeof fg.graphData === 'function' ? fg.graphData() : graphDataRef.current
106110
if (!data || !data.nodes) return
107111
data.nodes.forEach((node: GraphNode) => {
108112
if (node.x != null && node.y != null) {
@@ -151,16 +155,23 @@ export default function GraphView({
151155
return { nodes, links }
152156
}, [notes])
153157

158+
useEffect(() => {
159+
graphDataRef.current = graphData
160+
}, [graphData])
161+
154162
useEffect(() => {
155163
let attempts = 0
156-
const id = setInterval(() => {
164+
let timeoutId: number | null = null
165+
166+
const attemptForceSetup = () => {
157167
const fg = fgRef.current
158-
if (!fg) {
168+
if (!fg || typeof fg.d3Force !== 'function') {
159169
attempts++
160-
if (attempts > 20) clearInterval(id)
170+
if (attempts <= 20) {
171+
timeoutId = window.setTimeout(attemptForceSetup, 50)
172+
}
161173
return
162174
}
163-
clearInterval(id)
164175

165176
const folders = Array.from(new Set(graphData.nodes.map((n) => n.folder).filter(Boolean)))
166177
const centroids = buildFolderCentroids(folders)
@@ -187,9 +198,15 @@ export default function GraphView({
187198
)
188199
fg.d3Force('charge')?.strength(-120)
189200
fg.d3Force('collision', d3.forceCollide<GraphNode>(22))
190-
fg.d3ReheatSimulation()
191-
}, 50)
192-
return () => clearInterval(id)
201+
if (typeof fg.d3ReheatSimulation === 'function') {
202+
fg.d3ReheatSimulation()
203+
}
204+
}
205+
206+
timeoutId = window.setTimeout(attemptForceSetup, 50)
207+
return () => {
208+
if (timeoutId !== null) clearTimeout(timeoutId)
209+
}
193210
}, [graphData])
194211

195212
const handleNodeClick = useCallback(
@@ -212,8 +229,8 @@ export default function GraphView({
212229

213230
const focusOnNode = useCallback((nodeId: string) => {
214231
const fg = fgRef.current
215-
if (!fg) return
216-
const node = fg.graphData().nodes.find((n: GraphNode) => n.id === nodeId)
232+
if (!fg || typeof fg.graphData !== 'function' || typeof fg.cameraPosition !== 'function') return
233+
const node = fg.graphData()?.nodes?.find((n: GraphNode) => n.id === nodeId)
217234
if (!node || node.x == null || node.y == null) return
218235
fg.cameraPosition({ x: node.x, y: node.y, z: 120 }, { x: node.x, y: node.y, z: 0 }, 400)
219236
}, [])

0 commit comments

Comments
 (0)