Using GoJS with React
GoJS has a few key methods to help handle state management in React. In this intro, we'll demonstrate a basic setup for a Diagram component that is kept in sync with the rest of the React app. Our diagram will be using a GraphLinksModel and use a selection change listener, but these aren't requirements.
Examples of most of the topics discussed on this page can be found in the GoJS-React-Basic project, which serves as an simple starter project.
If you are new to GoJS, it may be helpful to first visit the Getting Started Tutorial.
Setting up a Diagram component
We suggest using a React class component implementing a few lifecycle methods. We also advise storing a ref to the diagram div and listeners in properties for access throughout the component.
export class Diagram extends React.Component<DiagramProps, {}> { private divRef: React.RefObject<HTMLDivElement>; private changedSelectionListener: ((e: go.DiagramEvent) => void) | null = null; // use a ChangedSelection listener private modelChangedListener: ((e: go.ChangedEvent) => void) | null = null; constructor(props: DiagramProps) { super(props); this.divRef = React.createRef(); } // convenience method to get the diagram from the div ref private getDiagram() { if (!this.divRef.current) return null; return go.Diagram.fromDiv(this.divRef.current); } // ... public render() { return (<div ref={this.divRef} className='diagram-component'></div>); } }
Diagram component props
When creating a React component for a GoJS Diagram, there are a few properties that the parent component should pass down as props:
- nodeDataArray: an array of node data objects
- linkDataArray: an array of link data objects
- modelData: an optional object for storing information for a model, independent of any node or link
- skipsDiagramUpdate: a boolean telling the diagram component that no updates are needed, usually set in state updates that came in from GoJS
- onDiagramChange: a handler function to deal with DiagramEvents, "ChangedSelection" in this case
- onModelChange: a handler function to update React state based on model changes inside GoJS
interface DiagramProps { nodeDataArray: Array<go.ObjectData>; linkDataArray: Array<go.ObjectData>; modelData?: go.ObjectData; skipsDiagramUpdate: boolean; onDiagramChange: (e: go.DiagramEvent) => void; onModelChange: (e: go.IncrementalData) => void; }
Initializing the GoJS Diagram
componentDidMount
The componentDidMount method is responsible for initializing the diagram and any listeners. It is important that the component be mounted because GoJS requires a DIV to render the diagram canvas. The model initialization starts with an empty GraphLinksModel with a GraphLinksModel.linkKeyProperty defined, then merges in the data arrays. This ensures any mutations done within GoJS don't directly affect React state.
It's important to keep React state up-to-date with any changes that have taken place in the GoJS model. The new method, Model.toIncrementalData, can be used within a model change listener in a similar manner to Model.toIncrementalJson, but contains deep copies of model objects rather than stringified JSON. This makes it easy to use to update React state.
We won't demonstrate a change handler/reducer here, as they will vary based on the structure of data. A basic setup can be seen in the GoJS-React-Basic project.
/** * Initialize the diagram and add the required listeners. */ public componentDidMount() { if (!this.divRef.current) return; const $ = go.GraphObject.make; const diagram = $(go.Diagram, this.divRef.current, // create a Diagram for the DIV HTML element { 'undoManager.isEnabled': true, // enable undo & redo model: $(go.GraphLinksModel, { linkKeyProperty: 'key' }) }); // define a simple Node template diagram.nodeTemplate = $(go.Node, 'Auto', // the Shape will go around the TextBlock $(go.Shape, 'RoundedRectangle', { strokeWidth: 0, fill: 'white' }, // Shape.fill is bound to Node.data.color new go.Binding('fill', 'color')), $(go.TextBlock, { margin: 8 }, // some room around the text // TextBlock.text is bound to Node.data.key new go.Binding('text', 'key')) ); // merge props into model to ensure a deep copy const model = diagram.model as go.GraphLinksModel; model.mergeNodeDataArray(this.props.nodeDataArray); model.mergeLinkDataArray(this.props.linkDataArray); if (this.props.modelData) { model.modelData = this.props.modelData; } // initialize listeners this.changedSelectionListener = (e: go.DiagramEvent) => { this.props.onDiagramChange(e); // or Redux action }; this.modelChangedListener = (e: go.ChangedEvent) => { if (e.isTransactionFinished) { this.props.onModelChange(e.model!.toIncrementalData(e)); // or Redux action } }; diagram.addDiagramListener('ChangedSelection', this.changedSelectionListener); diagram.addModelChangedListener(this.modelChangedListener); }
Updating the GoJS Model Based on React State Changes
componentDidUpdate
The componentDidUpdate method is where any changes to React state should be merged into the GoJS model. When state is updated in React, it is important to keep the GoJS model up-to-date. The methods used to do this are Model.mergeNodeDataArray and GraphLinksModel.mergeLinkDataArray. These methods take arrays of node or link data, iterate over those arrays, and merge any differences into the model.
/** * When the component updates, merge all data changes into the GoJS model to ensure everything stays in sync. * The model change listener is removed during this update since the data changes are already known by the parent. * @param prevProps * @param prevState */ public componentDidUpdate(prevProps: DiagramProps, prevState: any) { const diagram = this.getDiagram(); if (diagram) { const model = diagram.model as go.GraphLinksModel; // don't need model change listener while performing known data updates if (this.modelChangedListener) model.removeChangedListener(this.modelChangedListener); model.startTransaction('update data'); model.mergeNodeDataArray(this.props.nodeDataArray); model.mergeLinkDataArray(this.props.linkDataArray); if (this.props.modelData) { model.modelData = this.props.modelData; } model.commitTransaction('update data'); if (this.modelChangedListener) model.addChangedListener(this.modelChangedListener); } }
shouldComponentUpdate
The shouldComponentUpdate method can be used to perform comparisons between the props passed in to the Diagram component. This is also where one can check the skipsDiagramUpdate prop to prevent known updates.
/** * Determines whether component needs to update by comparing props and checking skipsDiagramUpdate. * @param nextProps * @param nextState */ public shouldComponentUpdate(nextProps: DiagramProps, nextState: any) { if (nextProps.skipsDiagramUpdate) return false; // quick shallow compare if (nextProps.nodeDataArray === this.props.nodeDataArray && nextProps.linkDataArray === this.props.linkDataArray && nextProps.modelData === this.props.modelData) return false; return true; }
Tearing Down the Diagram
componentWillUnmount
The componentWillUnmount method is responsible for tearing down the diagram and all listeners. It is important to set Diagram.div to null to remove all references to the diagram, and to remove any listeners.
/** * Disassociate the diagram from the div and remove listeners. */ public componentWillUnmount() { const diagram = this.getDiagram(); if (diagram) { diagram.div = null; if (this.changedSelectionListener) diagram.removeDiagramListener('ChangedSelection', this.changedSelectionListener); if (this.modelChangedListener) diagram.removeModelChangedListener(this.modelChangedListener); } }