Describe the bug
When using react-force-graph-3d inside a Next.js App Router application, navigating away from the graph page via browser back and then returning via browser forward causes the graph to become completely unresponsive — mouse drag, rotation, zoom, and node clicks all stop working. The graph canvas renders visually but no user interaction is registered.
This happens because react-kapsule's useEffectOnce uses useRef internally (effectCalled, renderAfterCalled) to guard against React 18 Strict Mode double-mounts. However, these refs persist when React reuses fibers during client-side navigation (e.g. Next.js App Router's popstate-based back/forward). On remount via fiber reuse:
effectCalled.current is still true from the previous lifecycle
- The guard
if (!effectCalled.current) prevents comp(domEl.current) from re-executing
- The kapsule instance is never re-initialized on the (potentially new) DOM element
- Meanwhile,
_destructor already ran during unmount — pausing animation and clearing data
- The component is stuck in a destroyed-but-not-reinitialized state
To Reproduce
- Set up a Next.js App Router project with
react-force-graph-3d
- Create a graph page (e.g.
/graph) that renders <ForceGraph3D />
- Navigate to
/graph — graph loads and interactions work normally
- Press the browser Back button to leave the page
- Press the browser Forward button to return to
/graph
- Attempt to drag, rotate, or click nodes on the graph
- All interactions are unresponsive — the graph is visually rendered but frozen
Note: navigating away by clicking an internal link and then pressing Back works fine, because Next.js creates a fresh fiber. The bug only occurs with back/forward navigation where React reuses the existing fiber.
Expected behavior
The ForceGraph component should fully re-initialize when remounted after browser back/forward navigation, with all mouse/touch interactions working as before.
Screenshots
N/A — the graph renders visually but all interaction is silently broken.
Desktop (please complete the following information):
- OS: macOS 15.3
- Browser: Chrome 133, Safari 18.3
- react-force-graph-3d: 1.29.1
- react-kapsule: 2.5.7
- 3d-force-graph: 1.79.1
- Next.js: 16.1.6
- React: 19.2.4
Additional context
The root cause is in react-kapsule@2.5.7's useEffectOnce (source):
function useEffectOnce(effect) {
var effectCalled = useRef(false); // ← persists on fiber reuse
var renderAfterCalled = useRef(false); // ← persists on fiber reuse
if (effectCalled.current) {
renderAfterCalled.current = true;
}
useEffectFn(function () {
if (!effectCalled.current) { // ← always true on fiber reuse → skips
destroyFunc.current = effect();
effectCalled.current = true;
}
// ...
return function () {
if (!renderAfterCalled.current) return;
if (destroyFunc.current) destroyFunc.current(); // ← _destructor runs on real unmount
};
}, []);
}
Possible fix: Reset effectCalled and renderAfterCalled inside the cleanup function after calling the destructor, so the next mount (whether via fiber reuse or fresh fiber) re-runs the initialization:
return function () {
if (!renderAfterCalled.current) return;
if (destroyFunc.current) destroyFunc.current();
// Reset for potential fiber reuse
effectCalled.current = false;
renderAfterCalled.current = false;
};
Current workaround: Force a fresh ForceGraph fiber on every remount using a key that increments when fiber reuse is detected:
const hasMountedRef = useRef(false);
const [graphKey, setGraphKey] = useState(0);
useEffect(() => {
if (hasMountedRef.current) {
setGraphKey(prev => prev + 1); // fiber reuse detected → force fresh instance
}
hasMountedRef.current = true;
}, []);
<ForceGraph3D key={graphKey} /* ... */ />
Related issue: vasturiano/3d-force-graph#732 — _destructor does not dispose Three.js controls/renderer/scene, which compounds this issue by leaking resources on every navigation cycle.
Describe the bug
When using
react-force-graph-3dinside a Next.js App Router application, navigating away from the graph page via browser back and then returning via browser forward causes the graph to become completely unresponsive — mouse drag, rotation, zoom, and node clicks all stop working. The graph canvas renders visually but no user interaction is registered.This happens because
react-kapsule'suseEffectOnceusesuseRefinternally (effectCalled,renderAfterCalled) to guard against React 18 Strict Mode double-mounts. However, these refs persist when React reuses fibers during client-side navigation (e.g. Next.js App Router'spopstate-based back/forward). On remount via fiber reuse:effectCalled.currentis stilltruefrom the previous lifecycleif (!effectCalled.current)preventscomp(domEl.current)from re-executing_destructoralready ran during unmount — pausing animation and clearing dataTo Reproduce
react-force-graph-3d/graph) that renders<ForceGraph3D />/graph— graph loads and interactions work normally/graphNote: navigating away by clicking an internal link and then pressing Back works fine, because Next.js creates a fresh fiber. The bug only occurs with back/forward navigation where React reuses the existing fiber.
Expected behavior
The ForceGraph component should fully re-initialize when remounted after browser back/forward navigation, with all mouse/touch interactions working as before.
Screenshots
N/A — the graph renders visually but all interaction is silently broken.
Desktop (please complete the following information):
Additional context
The root cause is in
react-kapsule@2.5.7'suseEffectOnce(source):Possible fix: Reset
effectCalledandrenderAfterCalledinside the cleanup function after calling the destructor, so the next mount (whether via fiber reuse or fresh fiber) re-runs the initialization:Current workaround: Force a fresh ForceGraph fiber on every remount using a key that increments when fiber reuse is detected:
Related issue: vasturiano/3d-force-graph#732 —
_destructordoes not dispose Three.js controls/renderer/scene, which compounds this issue by leaking resources on every navigation cycle.