vasturiano / react-force-graph

React component for 2D, 3D, VR and AR force directed graphs
https://vasturiano.github.io/react-force-graph/example/large-graph/
MIT License
2.17k stars 280 forks source link

Is there any way of caching __threeObj after first render? #204

Open giorgi-ghviniashvili opened 4 years ago

giorgi-ghviniashvili commented 4 years ago

I am rendering a component like this:


<ForceGraph3D
    ref={fgRef}
    width={dimensions.width}
    height={dimensions.height}
    graphData={data}
    nodeColor={c}
    nodeAutoColorBy={cl}
    linkAutoColorBy={cl}
    linkOpacity={settings.linkOpacity}
    onNodeDragEnd={node => {
      node.fx = node.x;
      node.fy = node.y;
      node.fz = node.z;
    }}
    nodeLabel="id"
    nodeThreeObject={(node) => {
      // here I tried to use cached mesh, but did not work
      if (meshes[node.id]) {
        return meshes[node.id];
      }
      return getNodeThreeObject(node, settings, c);
    }}
    onNodeClick={clickHandler}
    onBackgroundClick={resetHandler}
/>

getNodeThreeObject simply creates a new Mesh and adds SpriteText to that Mesh. On each `settings` change, I don't want to create a new Mesh and SpriteText object. I only want to update __threeObj after settings change. 

How can I achieve this?

In plain JavaScript, we can initialize ForceGraph and save the instance in the variable and then using methods on that instance, hence calling nodeThreeObject is called only once. But in react seems like we call nodeThreeObject everytime state changes?

Do you recommend switching to plain JavaScript and initialize graph only once in useEffect(() => {}, []) hook? 
vasturiano commented 4 years ago

@giorgi-ghviniashvili the function in nodeThreeObject only gets called once per node in the data, when that node is first added using graphData={...}.

In subsequent updates, the component detects existing nodes and will not call that function again if an object has already been created. This works the same way in either the React version or the plain JS component.

If you're seeing multiple calls for the same object, it could be that you're passing always a new set of (cloned) nodes at every render cycle. That would lead the component to assume it's a completely new data set and new objects need to be recreated.

giorgi-ghviniashvili commented 4 years ago

Hi, thanks for your answer.

The data never changes, I am requiring the data from local json file and passing as prop, that's it.

Only settings object changes in the parent component which leads re-rendering of the child component. I tested it multiple times, added console.log to the nodeThreeObject callback and it indeed gets called everytime the settings changes.

vasturiano commented 4 years ago

@giorgi-ghviniashvili I think I know why you're observing that behavior.

You should memoize your nodeThreeObject callback function, otherwise you're essentially re-defining a new function at every render iteration. And if the inner component detects a change in the nodeThreeObject callback it will invoke that new function for all the nodes, not just the new ones. That leads to the pattern you're seeing.

So, the fix should be as simple as this (if you're using react hooks):

nodeThreeObject={useCallback(node => { ... }, [])}
giorgi-ghviniashvili commented 4 years ago

@vasturiano Thanks. That was exactly the issue I was facing and fixed it.

I have another question, and I will ask it here instead of posting new issue.

Is there any way in the react api to access state.forceGraph onLoading, onFinishLoading functions Link

There is a wait time until the graph initializes all the nodeThreeObject and displays them, so I want to have loading indicator somehow. You have this loader when we use JsonUrl option, but in react, there is no JsonUrl and also no way to access the onLoading and onFinishLoading functions.

Also comparing performance with the same data in both react and no-react environment and non react is faster, would you recommend non-react setup (js module) when there is too much nodes and links?

Thanks a lot for your help and for the great library!

vasturiano commented 4 years ago

@giorgi-ghviniashvili glad to hear the callback memoization worked. 👍

As for the onLoading events that really is only relevant for the duration of the json fetch call when using jsonUrl. In React this module is just dedicated to the representation, leaving the fetching of data to the consuming application. So those loading events have no meaning in the React version. Just to be sure those do not cover the period that the module is rendering the objects, only for fetching data asynchronously.

In your application you should implement your own data fetching mechanism and loading indicators, leaving the responsibility of this component only to render the data.

Having said that, there's two events that you may find useful: onEngineTick and onEngineStop. At every rendering cycle (including the first time) onEngineTick will be triggered, so you could potentially use that event to detect when the component has started rendering, if that is important for your use case.

Regarding performance, with the same settings both components should behave and perform exactly the same, since react-force-graph-3d is just a wrapper around 3d-force-graph.

If you're observing differences it may be because of additional unnecessary re-renders that React can trigger if properties are not memoized, as was the case above. If you find a specific case in which it's not clear why the performances may be different, please post the two examples (on https://codesandbox.io/ f.e.) so we can have a closer look.