Issue with ReactDiagram not updating nodes after GoJS v3 update

Hello,

I’m encountering an issue after updating GoJS to version 3. In our functional React component, which uses ReactDiagram, nodes added after the diagram has been created are no longer displayed.

The component looks like this:

<ReactDiagram
  ref={diagramRef}
  divClassName={style.diagram}
  initDiagram={initDiagram}
  nodeDataArray={props.nodeDataArray}
  linkDataArray={props.linkDataArray}
  modelData={props.modelData}
  onModelChange={onModelChange}
  skipsDiagramUpdate={props.skipsDiagramUpdate}
/>

Initially, when props.nodeDataArray has values, the nodes render correctly. However, if props.nodeDataArray is updated later, the diagram does not reflect those changes. Also, the onModelChange is not triggered.

I have tested a sample from the GoJS React examples, and everything seems to work as expected there.

Given that props.nodeDataArray is being updated (I’ve confirmed that through debugging), I’m not sure why ReactDiagram isn’t updating to display the new nodes.

Could you advise on how I could debug this further or if there are any changes in v3 that might be causing this issue?

We are using

"gojs": "3.0.11",
"gojs-react": "1.1.2",
"react": "18.2.0",

This is the complete component:

import * as go from "gojs";
import { DiagramProps, ReactDiagram, ReactOverview } from "gojs-react";

import { useInjection } from "inversify-react";
import React, { useEffect, useRef } from "react";
import { ReviewDiagramAdornmentTemplate } from "./ReviewDiagramAdornmentTemplate";
import { ReviewDiagramNodeTemplate } from "./ReviewDiagramNodeTemplate";
import style from "./ReviewDiagramWrapper.module.css";

interface ReviewDiagramWrapperProps
    extends Pick<DiagramProps, "nodeDataArray" | "linkDataArray" | "modelData" | "skipsDiagramUpdate"> {
    /** Diagram background image */
    background?: Blob;
    /**
     * Function that is called when the model has changed.
     *
     * @param e The changed data
     * @param nodeDataArray The updated nodeDataArray
     */
    onModelChange: (e: go.IncrementalData, nodeDataArray: go.ObjectData[]) => void;
}

export const ReviewDiagramWrapper: React.FC<ReviewDiagramWrapperProps> = (props) => {
    const nodeTemplate = useInjection(ReviewDiagramNodeTemplate);
    const adornmentTemplate = useInjection(ReviewDiagramAdornmentTemplate);

    const diagramRef = useRef<ReactDiagram>(null);

    const getDiagram = () => diagramRef.current?.getDiagram();

    useEffect(() => {
        console.log("ReviewDiagramWrapper props changed", props.nodeDataArray, props.skipsDiagramUpdate);
    }, [props.nodeDataArray, props.skipsDiagramUpdate]);

    const initDiagram = () => {
        const diagram = new go.Diagram({
            "undoManager.isEnabled": true, // must be set to allow for model change listening
            "undoManager.maxHistoryLength": 0, // disable undo/redo functionality
            model: new go.GraphLinksModel({
                linkKeyProperty: "key", // Must be defined for merges and data sync when using GraphLinksModel
            }),
            "animationManager.isEnabled": false,
            // allowMove: false,
        });

        diagram.nodeTemplate = nodeTemplate.create();
        diagram.nodeSelectionAdornmentTemplate = adornmentTemplate.create();

        return diagram;
    };

    const initOverview = () => {
        return new go.Overview({ contentAlignment: go.Spot.Center });
    };

    const onModelChange = (e: go.IncrementalData) => {
        console.log("Model changed", e, getDiagram()?.model.nodeDataArray);
        props.onModelChange(e, getDiagram()?.model.nodeDataArray ?? []);
    };

    return (
        <div style={{ position: "relative", display: "flex", flex: 1 }}>
            <ReactDiagram
                ref={diagramRef}
                divClassName={style.diagram}
                initDiagram={initDiagram}
                nodeDataArray={props.nodeDataArray}
                linkDataArray={props.linkDataArray}
                modelData={props.modelData}
                onModelChange={onModelChange}
                skipsDiagramUpdate={props.skipsDiagramUpdate}
            />
            <ReactOverview
                initOverview={initOverview}
                divClassName={style.overviewComponent}
                style={props.background == null ? { display: "none" } : undefined}
                observedDiagram={getDiagram() ?? null}
            />
        </div>
    );
};

ReviewDiagramWrapper.displayName = "DiagramWrapper";

export default ReviewDiagramWrapper;

Just to be sure, are you making immutable updates to your data arrays? I don’t see anything in your provided code that should cause problems in 3.0. What are your template definitions?

Also, are you seeing any errors or warnings in the console?

Hi,
basically the initial nodeDataArray is empty and after a service call a new array is created and it completely replaces the old nodeDataArray.
There are no errors or warnings in the console. The templates are distributed over several files. I could merge and provide them but I think the issue must be something else:

I did some further investigations and found out that the ref in my DiagramWrapper is set to the ReactDiagram correct but when I call ref.getDiagram() it is always null. However, I can see the (empty) diagram and for example start the drag Selection.

Any idea in which case the component would return null for getDiagram even though it is mounted?

Here’s what I tried: https://codesandbox.io/p/sandbox/stoic-lederberg-c2kdf4

I wonder if your getDiagram arrow function has a stale reference. Can you try putting a breakpoint within it?

The diagramRef.current has a reference to the ReactDiagram component.
However, the diagramRef.current.getDiagram() returns null.

Not sure if this helps:

The React wrapper seems to call fromDiv, and in fromDiv, it fails to create the Diagram from the div element and returns null.

grafik

This is the div:

<div class="ReviewDiagramWrapper-module_diagram_sCeE" style="position: relative; -webkit-tap-highlight-color: rgba(255, 255, 255, 0);"><canvas tabindex="0" width="391" height="1031" style="position: absolute; top: 0px; left: 0px; z-index: 2; user-select: none; touch-action: none; width: 391px; height: 1031px;"></canvas><div style="position: absolute; overflow: auto; width: 391px; height: 1031px; z-index: 1;"><div style="position: absolute; width: 1px; height: 1px;"></div></div></div>

That’s odd. I haven’t been able to reproduce it.

Can you send us the simplest reproduction of the issue you can produce? You can email gojs AT nwoods.com if there’s anything you don’t want public.

So first of all, the issue starts to occur with goJS v2.3.15. Until 2.3.14 it works correctly.

The issue seems to occur because there a two instances of goJS, as I am also getting this message in the console:
WARNING: a go object on the root object is already defined. debug version: 2.3.19, replaced with version: 2.3.19

We are using the gojs-react library to create a goJS Diagram in React but also using the gojs library directly to create a goJS diagram with plain HTML/JQuery in the same app.
The React diagram is using the ES Module bundle of gojs (release/go-debug.mjs) while the plain HTML diagram is using the CommonJS bundle of gojs (release/go-debug.js)

We are using webpack to bundle our dependencies. As we don’t set a target in our webpack configuration, it will use mainFields: ["browser", "module", "main"] for resolving dependencies (see Resolve | webpack)

From here on I am not so sure but if I understand it correctly, the behavior is as follows:

The gojs-react package.json only specifies main and module. So when importing the gojs-react package, the browser mainField will be skipped and it will use the module entry point. The gojs-react module then uses the gojs ES module bundle.

The gojs package.json, on the other side, additionally specifies an exports section with import and default. So when importing the gojs package, the default definition will be used for the browser entry point (pointing to the CommonJS bundle).

This is the reason why we have two instances of goJS.

In 2.3.15 we added this to the GoJS package.json file:

  "exports": {
    "import": "release/go.mjs",
    "require": "release/go.js"
  },

In 2.3.16 we improved that “exports” section as follows:

  "exports": {
    "import": {
      "types": "./release/go-module.d.ts",
      "development": "./release/go-debug.mjs",
      "production": "./release/go.mjs",
      "default": "./release/go.mjs"
    },
    "require": {
      "types": "./release/go.d.ts",
      "development": "./release/go-debug.js",
      "production": "./release/go.js",
      "default": "./release/go.js"
    }
  }

So, why isn’t it that you aren’t using the same library for both uses? Either use ES modules or use UMD consistently.

We currently compile our app to commonjs.
Our TypeScript codes generates require statements to load dependencies.
And we use webpack to bundle our code. Default configuration how webpack decides which fields to use as main entry point is mainFields: ["browser", "module", "main"]

In 2.3.14, there was no exports property in your package.json, so webpack uses the mainFields.As there is no browser , it uses the module entry point, i.e. release/go-module.js (ESM)

In 2.3.15, there is an exports property in your package.json, and since our code uses require, it will use exports.requirewhich is ./release/go.js (CommonJS)

In 2.3.19, there is an exports property in your package.json but no require, so it will use .default.production which is ./release/go.js (CommonJS)

gojs-react library has no exports property, so webpack uses module which is lib/esm/gojsreact.js. (ESM). As this is using import statements, I assume that this will lead to the inclusion of the ESM version of gojs.

We are currently evaluating switching our app to ESM. This fixes the issue for us as only the ESM version of gojs is bundled.

So if I understand this correctly, it is the gojs-react package that we need to update somehow to control which module system it uses, even though gojs is a peer dependency.

FYI, when we released v3.0, we removed the extensions (in the extensionsTS folder) that were compiled to use require. Now the only included extensions are in the extensions folder for <script> tags and the extensionsJSM folder where the TypeScript source code is for all of the extensions, and where the compiled .js files use import. Presumably anyone using the TypeScript extension source files will compile it in whatever manner is suitable for their project, and anyone using the .js files there will be bundled appropriately for their project.

Maybe, I don’t know how it exactly works but at least for me with the same webpack configuration it loaded different libraries.

Then again, not many people will use both gojs and gojs-react at the same time (we are doing so because we are gradually migrating our app from JQuery to React), so it might not be that relevant.

Also, we successfully switched to ESM output so the issue is solved for us now.