From 6f2dcd09ffb89ffae0773de1bfaaa6710b555d21 Mon Sep 17 00:00:00 2001
From: Dennis Collaris <d.a.c.collaris@uu.nl>
Date: Tue, 12 Nov 2024 11:25:29 +0000
Subject: [PATCH] fix: properly release canvas/gl contexts when component
 unmounts

---
 .../matrixvis/components/MatrixPixi.tsx       | 69 +++++++++----------
 .../nodelinkvis/components/NLPixi.tsx         |  5 +-
 2 files changed, 35 insertions(+), 39 deletions(-)

diff --git a/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx b/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx
index d2c816f16..d64a8bb2a 100644
--- a/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx
+++ b/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx
@@ -2,7 +2,7 @@ import { Edge, GraphQueryResult, Node, useML } from '@graphpolaris/shared/lib/da
 import { dataColors, visualizationColors } from 'config';
 import { Viewport } from 'pixi-viewport';
 import { Application, ColorSource, Container, FederatedPointerEvent, Graphics, IPointData, Point, Text } from 'pixi.js';
-import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from 'react';
+import { useEffect, useRef, useState, useMemo, useImperativeHandle, forwardRef } from 'react';
 import { LinkType, NodeType } from '../types';
 import { NLPopup } from './MatrixPopup';
 
@@ -63,10 +63,11 @@ export const MatrixPixi = forwardRef((props: Props, refExternal) => {
   const globalConfig = useConfig();
 
   useEffect(() => {
-    if (props.graph) {
-      setup();
+    if (props.graph && internalRef.current && imperative.current) {
+      if (isSetup.current === false) setup();
+      else update();
     }
-  }, [globalConfig.theme]);
+  }, [props.graph, globalConfig.theme]);
 
   let columnOrder: string[] = [];
   let rowOrder: string[] = [];
@@ -82,6 +83,20 @@ export const MatrixPixi = forwardRef((props: Props, refExternal) => {
   const isSetup = useRef(false);
   const ml = useML();
 
+
+  const app = useMemo(
+    () =>
+      new Application({
+        backgroundAlpha: 0,
+        antialias: true,
+        autoDensity: true,
+        eventMode: 'auto',
+        resolution: window.devicePixelRatio || 2,
+        view: canvas.current as HTMLCanvasElement,
+      }),
+    [canvas.current],
+  );
+
   useEffect(() => {
     if (typeof refExternal === 'function') {
       refExternal(internalRef.current);
@@ -135,13 +150,11 @@ export const MatrixPixi = forwardRef((props: Props, refExternal) => {
     },
   }));
 
-  let app: Application;
-
   function resize() {
     const width = internalRef?.current?.clientWidth || 1000;
     const height = internalRef?.current?.clientHeight || 1000;
 
-    app.renderer.resize(width, height);
+    app?.renderer.resize(width, height);
     if (viewport.current) {
       viewport.current.screenWidth = width;
       viewport.current.worldWidth = width;
@@ -152,22 +165,23 @@ export const MatrixPixi = forwardRef((props: Props, refExternal) => {
     if (props.graph) {
       setup();
     }
-
-    app.render();
+    app?.render();
   }
-
+  
   useEffect(() => {
-    if (props.graph && internalRef.current && internalRef.current.children.length > 0) {
-      if (!isSetup.current) setup();
-      else update();
+    return () => {
+      app.destroy();
     }
-  }, [props.graph]);
+  }, []);
 
   useEffect(() => {
-    if (props.graph && internalRef.current && internalRef.current.children.length > 0) {
-      setup();
-    }
-  }, [props.settings]);
+    if (!internalRef.current) return;
+    const resizeObserver = new ResizeObserver(() => {
+      resize();
+    });
+    resizeObserver.observe(internalRef.current);
+    return () => resizeObserver.disconnect();
+  }, []);
 
   // TODO implement search results
   // useEffect(() => {
@@ -306,22 +320,6 @@ export const MatrixPixi = forwardRef((props: Props, refExternal) => {
   const setup = () => {
     if (!props.graph) throw Error('Graph is undefined; setup not possible');
 
-    if (app == null) {
-      app = new Application({
-        backgroundAlpha: 0,
-        antialias: true,
-        autoDensity: true,
-        eventMode: 'auto',
-        resolution: window.devicePixelRatio || 1,
-        view: canvas.current as HTMLCanvasElement,
-      });
-
-      const resizeObserver = new ResizeObserver(() => {
-        resize();
-      });
-      resizeObserver.observe(internalRef.current as HTMLDivElement);
-    }
-
     if (svg.current != null) {
       select(svg.current).selectAll('*').remove();
     }
@@ -409,8 +407,9 @@ export const MatrixPixi = forwardRef((props: Props, refExternal) => {
     viewport.current.drag().pinch().wheel().animate({}).decelerate({ friction: 0.75 });
 
     app.ticker.add(tick);
-    update();
     isSetup.current = true;
+
+    update();
   };
 
   let scaleColumns: ScaleBand<string>;
diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
index db3a48133..d39dd29fa 100644
--- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
+++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx
@@ -774,10 +774,7 @@ export const NLPixi = forwardRef((props: Props, refExternal) => {
     return () => {
       nodeMap.current.clear();
       linkLabelMap.current.clear();
-      linkGfx.clear();
-      nodeLayer.removeChildren();
-      linkLabelLayer.removeChildren();
-      nodeLabelLayer.removeChildren();
+      app.destroy();
 
       const layout = layoutAlgorithm.current as GraphologyForceAtlas2Webworker;
       if (layout?.cleanup != null) layout.cleanup();
-- 
GitLab