Link reshaped points not loading when using gojs-react

I am having a problem where links that have been reshaped do not reload using the stored points, but go back to the default routing points. Overall my implementation looks a lot like the Dynamic Ports sample, but using React, and the gojs-react npm component.

I am using model.toJson and model.fromJson, and storing in a database between reloads. I can see in the database that links in the links have points that update as I reshape the link. Here is a sample of the first link in the database:

  "linkDataArray": [
      "id": "cf7ae4e7-1b9d-4a36-9985-8e6604a1f57c",
      "to": "ea4900e0-0817-4d4f-9fa7-d28ec88ff904",
      "from": "0bd4201f-be65-49dc-a1ee-96257a100879",
      "text": "FIRST",
      "points": [
      "toPort": "_01",
      "fromPort": "01",
      "segmentIndex": {
        "class": "NaN"
      "segmentFraction": 0.21681827187411645

My link template does have the required binding, here it is in full:

	diagram.linkTemplate =
		goObject( CustomLink,
				routing: go.Link.AvoidsNodes,
				corner: 8,
				curve: go.Link.JumpGap,
				reshapable: true,
				resegmentable: false,
				relinkableFrom: true,
				relinkableTo: true
			new go.Binding( "points" ).makeTwoWay(),
			goObject( go.Shape, { stroke: "#2F4F4F", strokeWidth: 2 } ),
			goObject( go.Shape, { toArrow: "Feather" } ),
			goObject( go.Panel, "Auto",
				{ visible: false, _isLinkLabel: true, segmentIndex: NaN, segmentFraction: 0.5 }, // Default not visible
				new go.Binding( "visible", "text", text => !!text ),
				goObject( go.Shape, { stroke: "#2F4F4F", fill: '#ffffff', strokeWidth: 0.5 } ),
				goObject( go.Panel, "Horizontal", { margin: new go.Margin( 3, 3, 0, 3 ) }, // Panel needed for inner margins.
					goObject( go.TextBlock, "",  // the label text
							textAlign: "center",
							font: "9pt helvetica, arial, sans-serif",
							stroke: "#2F4F4F",
							editable: true
						new go.Binding( "text" ).makeTwoWay()
				new go.Binding( "segmentFraction" ).makeTwoWay()

The linkDataArray is then fed into the React component:


…but when the diagram is drawn all link reshapes are ignored and they are routed with the default path.

(This is with “gojs”: “^2.1.30”, “gojs-react”: “^1.0.10” )

What is the value of links being passed into the ReactDiagram component? Are the points properties just arrays of numbers, like the saved JSON, or are they GoJS lists of Points?

Actually, that may not matter. I want to get a better idea of what your process for saving/loading is. Do you have some kind of load button on the page? What code does it execute?

Saving I think is not the issue, I can clearly see reshaped coordinates in the point array in the database after saving.

Loading is a little too involved to show the actual code (via GraphQL authenticated by OIDC etc) but in the end I end up with a JavaScript object named network created from the database JSON, that is loaded in a React useEffect like so:

				set_nodes( network.nodeDataArray )
				set_links( network.linkDataArray )
				set_skipsDiagramUpdate( false )
				gojsDiagramObject.current.model = go.Model.fromJson( JSON.stringify( network ) )
				gojsDiagramObject.current.model.makeUniqueLinkKeyFunction = () => uuidv4()

The set_ calls are React useState set methods.

(The .current is due to the diagram object being stored in a useRef() )

The links and nodes in the ReactDiagram above are those states.

And to answer your first question, what happens is that in the initial render of <ReactDiagram> the link points is an array of numbers, but then there is an immediate onModelChange call that turns it into a collection of Points.

And I probably should point out that except for link reshaping all other aspects of the diagram persistence work very well, including for example link label segmentFraction.

Are the loaded node locations different from the diagram’s node locations at the time the load is occuring? For instance, has the user moved a node after saving its location? That’s likely causing the routes to be invalidated when the node is moved back to the saved location.

We are thinking about ways to ensure any saved routes can be faithfully restored if the programmer wishes.

Nope. This all occurs at page load, no user interaction.
I did some more throughout logging of the loading sequence shown below.
I also log the .points for the first link in the linkDataArray
The sequence below is part of page load, no user interaction.

GoDiagram.js?180c:49 <ReactDiagram> render
network.js?e4d7:165 go.Model.fromJson()
GoDiagram.js?180c:49 <ReactDiagram> render
GoDiagram.js?180c:50 (12) [-124.14285278320312, -55, -106.14285278320312, -55, -20.33333396911621, -55, -20.33333396911621, 97.5, -16.857147216796875, 97.5, -10.857147216796875, 97.5]
network.js?e4d7:364 handleModelChange {modifiedNodeData: Array(10), modifiedLinkData: Array(18)}
network.js?e4d7:339 handler modifiedLinkData
GoDiagram.js?180c:49 <ReactDiagram> render
GoDiagram.js?180c:50 E {__gohashid: 2895, s: false, j: Array(6), Aa: 1, Ga: null, …}Aa: 1Ga: nulleh: nullj: Array(6)0: J {x: -124.14285278320312, y: -55, s: true}1: J {x: -106.14285278320312, y: -55, s: true}2: J {x: -20.33333396911621, y: -55, s: true}3: J {x: -20.33333396911621, y: 97.5, s: true}4: J {x: -16.857147216796875, y: 97.5, s: true}5: J {x: -10.857147216796875, y: 97.5, s: true}length: 6__proto__: Array(0)s: false__gohashid: 2895count: (...)iterator: (...)iteratorBackwards: (...)length: (...)size: (...)_dataArray: (...)__proto__: Object
network.js?e4d7:364 handleModelChange {modifiedNodeData: Array(10), modifiedLinkData: Array(18)}
network.js?e4d7:339 handler modifiedLinkData
GoDiagram.js?180c:49 <ReactDiagram> render
GoDiagram.js?180c:50 E {__gohashid: 3940, s: false, j: Array(6), Aa: 1, Ga: null, …}Aa: 1Ga: nulleh: nullj: Array(6)0: J {x: -124.14285278320312, y: -55, s: false}1: J {x: -106.14285278320312, y: -55, s: false}2: J {x: -61.5, y: -55, s: false}3: J {x: -61.5, y: 97.5, s: false}4: J {x: -16.857147216796875, y: 97.5, s: false}5: J {x: -10.857147216796875, y: 97.5, s: false}length: 6__proto__: Array(0)s: false__gohashid: 3940count: (...)iterator: (...)iteratorBackwards: (...)length: (...)size: (...)_dataArray: (...)__proto__: Object

Analysis by line:

1 First render on mount. No data loaded yet.
2 Data has loaded in parent component which runs fromJson
3-4 Second render now with .point as a plain array
5-6 modelChanged called
7-8 Third render, now with .points as collection of Points. NB! x: -20.333 coordinate! This is the reshaped segment.
9-10 Another modelChanged called.
11-12 Fourth render. NB! X coordinate of this segment is now -61.5 for no apparent reason. This is the x coordinate where it sits if un-reshaped.

I notice you are setting gojsDiagramObject.current.model. I wouldn’t expect that to be necessary, as you should just be able to pass the node and link data to the component to update the existing model that gets initialized on mount. Also, make sure skipsDiagramUpdate is being set to true in your handleModelChange function. This ensures you won’t pass the component data that it already has.

I don’t think any of these points explain why the <ReactDiagram> seems to be resetting link reshapes…

Anyways, I was thinking the same thing regarding fromJSON, but for some reason the group membership is not getting picked up if I remove the gojsDiagramObject.current.model = go.Model.fromJson(...) line. I cannot see the reason for this, since it uses exactly the same nodeDataArray as what is passed to the react diagram. And it has the correct group/isGroup data.

Example if I remove the .fromJSON() call:

Example with exactly the same data when doing .fromJSON():

Yes. If this is not set all hell breaks loose :-D

Just for fun I made my handleModelChange function do absolutely nothing.

Then the diagram loads just fine with the links reshaped.

So my problem boils down to why <ReactDiagram> thinks it needs to send two modelChanged events on load:

  • The first to convert .points from a plain array to a Points collection (which is fine I guess)
  • The second to reset all link reshapes to default path. Why??

I’m seeing if I can reproduce the problem in a more simple scenario. If I can’t it, may help if you could send us your code so we can debug.

Thanks. Doing a React version of the Dynamic Ports sample which uses persisted reshaped links should get you fairly close to what I have.

Here’s a CodeSandbox demonstrating something like Dynamic Ports, with state containing link points for the two links between unit One and unit Two. As you can see, the routes are preserved during the diagram load.

So I’m not sure what your app may be doing differently to cause those saved points to be overwritten.

Answer: Async load.

I messed a little with your sanbox.

First I put my sample diagram in there. That works fine (looks like shit because of the templates, but never mind)

Then I made it first load an empty diagram, then update state after a few seconds.
And voila, reshaped links are gone.

Ok. That means your nodes don’t get locations until after the initial load, and so your links want to reroute to accomodate node locations. This doesn’t happen during the initial load because we save link routes during that time. We’ll test adding a prop to ReactDiagram that will let you treat renders beyond the mount as the initial load so you can control this.

Hmm. Ok, so now knowing why it is doing this, I can probably fix it by not mounting the component until data is loaded. I will try this.

Yes, I imagine that could also work. Let us know, as if we don’t need to add the new prop, we won’t.

Works like a charm. I think if you document this not-so-obvious behavior you’re good to go.

Oh and by the way, this also fixes the weirdness with .fromJSON, which is now not needed any more.

So both group membership and reshaped links are things that will only work if present at component mount, not when loaded in later steps.

Great, we’ll put some clarifications in the docs for gojs-react. Thanks for letting us know about this!