plotly / react-cytoscapejs

React component for Cytoscape.js network visualisations
MIT License
483 stars 68 forks source link

examples using hooks / useState ? #68

Open dcsan opened 3 years ago

dcsan commented 3 years ago

I'm trying to create a functional component with this lib, and useState hooks, but having problems dealing with the cy instance. I need to add events to the cy instance, eg tap. but this seems to create a memory leak - now each time the graph is rendered again, there's another 'graph' created. tapping will now send 3, 4, ... etc events.

what's the best way to manage this? do i need to put some code in to try and remove the cy.on(...) event before the next render?

Thanks!

code is like this below

import React, { useState, useEffect } from 'react';

import Cytoscape from 'cytoscape';
import CytoscapeComponent from 'react-cytoscapejs'
import cola from 'cytoscape-cola';
// import cydagre from 'cytoscape-dagre';
import { graphStyle, nodeStyle } from './graphStyle'

const layout = {
  name: 'cola'
}
Cytoscape.use(cola);

// import { DcLib } from '../utils/DcLib'

const KGraph = (props: any) => {
  const graph = props?.graph
  const [cy, setCy] = useState({})

  useEffect(() => {
    console.log('effect')
    // cy.layout(layout).run()
  })

  console.log('graph', graph)

  const initCy = (cy: any) => {
    // @ts-ignore
    console.log('initCy')
    if (!cy) {
      setCy(cy)
    }

    cy.on('tap', (event: any) => {
      console.log('tap cy', event)
      console.log('target', event.target)
    });
  }

  if (!props.graph.ready) {
    return (<div> graph here </div>)
  }

  const layoutGraph = () => {
    // @ts-ignore
    cy.layout(layout).run()
  }

  return (
    <div>
      <button onClick={() => layoutGraph()}>redo graph</button>
      <CytoscapeComponent
        cy={initCy}
        elements={graph.elements}
        style={graphStyle}
        layout={layout}
        stylesheet={nodeStyle}
      />
    </div>
  )

}

export default KGraph
williaster commented 3 years ago

I don't think I had this problem in my application using useRef with a couple of checks and cleanup.

function MyComponent() {
  const cyRef = useRef<CytoscapeRef | null>();

  // cleanup cytoscape listeners on unmount
  useEffect(() => {
    return () => {
      if (cyRef.current) {
        cyRef.current.removeAllListeners();
        cyRef.current = null;
      }
    };
  }, []);

  const cyCallback = useCallback(
    (cy: CytoscapeRef) => {
      // this is called each render of the component, don't add more listeners
      if (cyRef.current) return;

      cyRef.current = cy;
      cy.ready(...);
      cy.on(...);
    },
    [...dependencies],
  );

  return <Cytoscape cy={cyCallback} {...} />;
}
dcsan commented 3 years ago

hmm ok useRef always feels a bit dirty to me like you're changing stuff that react wants to control, and it will bite you eventually...

zirkelc commented 3 years ago

@williaster your code works well for me!