React- onModelChange for Merging Data

Hello, I’m running into initialization issues trying to use GoJS within a React based wrapper component when opening saved data. When opening existing diagrams, we add our node and link data to the props of the ReactDiagram component and just need the diagram to draw it; the problem we’re seeing is that when the component is mounting, we’re getting extra onModelChange events, likely due to the call to “mergeData” within GoJS code. This is the code that I am talking about:

ReactDiagram.prototype.componentDidMount = function () {
        var _this = this;
        if (this.divRef.current === null)
            return;
        var diagram = this.props.initDiagram();
        diagram.div = this.divRef.current;
        this.modelChangedListener = function (e) {
            if (e.isTransactionFinished && e.model && !e.model.isReadOnly && _this.props.onModelChange) {
                var dataChanges = e.model.toIncrementalData(e);
                if (dataChanges !== null)
                    _this.props.onModelChange(dataChanges);
            }
        };
        diagram.addModelChangedListener(this.modelChangedListener);
        diagram.delayInitialization(function () {
            _this.mergeData(diagram, true);
        });
    };

It appears that the addModelChangedListener is added before the data is merged, which ends up in an extra call to our onModelChanged prop telling us that we have changes, when in actuality we don’t since this is a first render. This is causing a large performance problem within our app because we can cannot distinguish easily between a first render and an actual model change for gojs.

Is there any way that the addModelChangedListener can be set after the data has been merged so we aren’t getting an event of a change that didn’t happen? Or through debugging this code, I see that you guys have a prop for when it is an initial render on the event model- can the event be exposed as an optional prop on the onModelChange handler so we can know when to skip updates?

Do you mean the skipsDiagramUpdate property? Yes, that is in the DiagramProps interface.

Your onModelChange handler, when it updates the app’s state, will want to set skipsDiagramUpdate to true so that it doesn’t bother trying to update with changes that the user caused.

No, I’m not referring to that flag, more so before when that flag gets used. When initializing a new diagram, if we provide data in the nodeDataArray and/or linkDataArray props we get notified of an “onModelChange” event saying that data has changed for the graph; however, to us the data is exactly as we provided it to be. We need some indication in our wrapper code to know that it was either an initial render and we don’t have to update our store or not receive an event at all for this.

I understand the skipsDiagramUpdate flag is to prevent the graph from thinking more changes have occurred and getting stuck in an update loop, but the consumer code will still get updated because we’re being told a new event occurred. (From gojs docs: gojs-react-basic/src/App.tsx at 44dd9beb1576f7a7de20f1c7238ad2634892d439 · NorthwoodsSoftware/gojs-react-basic · GitHub).

For some background, we can have diagrams with thousands of nodes and connections- a single change to one of the nodes causes a downstream affect of changes in our app that are very slow to calculate. When opening a new diagram, because we are getting this extra notification of a model change to everything in the graph, it’s forcing us to unnecessarily do an update on every node and connection, which is tanking performance

I haven’t tried this in a React app, but can’t you set a flag when you are wanting to show a new diagram, and have your update code do nothing when that flag is true?

We can: the problem for us is knowing when to reset or reinstate that flag. We’ve been trying to use the IncrementalData prop to determine if it’s an initial render or not, but that isn’t foolproof. GoJS also isn’t the only view in our app- people can click away and back to our GoJS component and if a rerender occurs, we don’t have a way of distinguishing then that it’s the same as opening a new diagram.

If we have the “reason” returned on the onModelChange of it being an initialRender, I think it’d save us a lot of trouble.

In those other cases, how can the diagram know whether it’s the first time or not?

The general answer is to check to see whether the data is different or not. But it would be more efficient not to do that onModelChange call, so it might be worth defining the cases where you think “it’s the first time” and maybe we can figure out how to detect that.

In this code (which you asked about above):

    // initialize data change listener
    this.modelChangedListener = (e: go.ChangedEvent) => {
      if (e.isTransactionFinished && e.model && !e.model.isReadOnly && this.props.onModelChange) {
        const dataChanges = e.model.toIncrementalData(e);
        if (dataChanges !== null) this.props.onModelChange(dataChanges);
      }
    };

you could put an additional check to see whether it thinks it’s an initial transaction:

      if (e.isTransactionFinished && e.model && !e.model.isReadOnly &&
          this.props.onModelChange && e.oldValue !== 'initial merge') {

That transaction name is determined in the private mergeData method.

BUT the reason that the onModelChange function is always called is that often the act of loading the model will end up modifying it. One example is if your nodes have TwoWay Bindings on the Node.location property, and you want the automatic layout to compute and remember the locations of the nodes for your app.