Skip to content

ForceGraph becomes unresponsive after browser back/forward navigation (React fiber reuse breaks useEffectOnce re-initialization) #596

Description

@wannysim

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:

  1. effectCalled.current is still true from the previous lifecycle
  2. The guard if (!effectCalled.current) prevents comp(domEl.current) from re-executing
  3. The kapsule instance is never re-initialized on the (potentially new) DOM element
  4. Meanwhile, _destructor already ran during unmount — pausing animation and clearing data
  5. The component is stuck in a destroyed-but-not-reinitialized state

To Reproduce

  1. Set up a Next.js App Router project with react-force-graph-3d
  2. Create a graph page (e.g. /graph) that renders <ForceGraph3D />
  3. Navigate to /graph — graph loads and interactions work normally
  4. Press the browser Back button to leave the page
  5. Press the browser Forward button to return to /graph
  6. Attempt to drag, rotate, or click nodes on the graph
  7. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions