Using GoJS with React
Examples of most of the topics discussed on this page can be found in the gojs-react-basic project, which serves as a simple starter project.
If you are new to GoJS, it may be helpful to first visit the Getting Started Tutorial.
GoJS has a few key methods to help handle state management in React. In this intro, we'll demonstrate a couple options for a basic setup for a component that is kept in sync with the rest of the React app. Our examples will be using a GraphLinksModel, but any model can be used.
Using the gojs-react package
The easiest way to get a component set up for a GoJS Diagram is to use the gojs-react package, which exports React Components for GoJS Diagrams, Palettes, and Overviews. The gojs-react-basic project demonstrates how to use these components. More information about the package can be found on the Github or NPM pages.
import * as go from 'gojs'; import { ReactDiagram } from 'gojs-react'; import * as React from 'react'; // ... // props passed in from a parent component holding state interface DiagramProps { nodeDataArray: Array<go.ObjectData>; linkDataArray: Array<go.ObjectData>; modelData: go.ObjectData; skipsDiagramUpdate: boolean; onDiagramEvent: (e: go.DiagramEvent) => void; onModelChange: (e: go.IncrementalData) => void; } export class DiagramWrapper extends React.Component<DiagramProps, {}> { /** * Ref to keep a reference to the component, which provides access to the GoJS diagram via getDiagram(). */ private diagramRef: React.RefObject<ReactDiagram>; constructor(props: DiagramProps) { super(props); this.diagramRef = React.createRef(); } // ... public render() { return ( <ReactDiagram ref={this.diagramRef} divClassName='diagram-component' initDiagram={this.initDiagram} nodeDataArray={this.props.nodeDataArray} linkDataArray={this.props.linkDataArray} modelData={this.props.modelData} onModelChange={this.props.onModelChange} skipsDiagramUpdate={this.props.skipsDiagramUpdate} /> ); } }
Setting up your own component
In some cases, you may want to implement your own component, in which case we suggest using a React 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 ReactDiagram extends React.Component<DiagramProps, {}> { private divRef: React.RefObject<HTMLDivElement>; 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 public getDiagram() { if (this.divRef.current === null) return null; return go.Diagram.fromDiv(this.divRef.current); } // ... public render() { return (<div ref={this.divRef} className='diagram-component'></div>); } }
React 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
- 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; 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. The merge will make a shallow copy of data, so if you're using data with nesting, you will probably want to clone your arrays before passing them, maybe using Model.cloneDeep.
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 === null) 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 (it may be useful to use Model.cloneDeep if using nested properties in your data) const model = diagram.model as go.GraphLinksModel; model.commit((m: go.Model) => { m.mergeNodeDataArray(this.props.nodeDataArray); m.mergeLinkDataArray(this.props.linkDataArray); if (this.props.modelData !== undefined) { m.assignAllDataProperties(m.modelData, this.props.modelData); } }, null); // initialize listeners this.modelChangedListener = (e: go.ChangedEvent) => { if (e.isTransactionFinished) { const dataChanges = e.model!.toIncrementalData(e); if (dataChanges !== null) this.props.onModelChange(dataChanges); // or Redux action } }; 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 !== null) { const model = diagram.model as go.GraphLinksModel; // don't need model change listener while performing known data updates if (this.modelChangedListener !== null) model.removeChangedListener(this.modelChangedListener); model.startTransaction('update data'); // maybe deep copy if using nested data! model.mergeNodeDataArray(this.props.nodeDataArray); model.mergeLinkDataArray(this.props.linkDataArray); if (this.props.modelData !== undefined) { model.assignAllDataProperties(model.modelData, this.props.modelData); } model.commitTransaction('update data'); if (this.modelChangedListener !== null) model.addChangedListener(this.modelChangedListener); } }
shouldComponentUpdate
The shouldComponentUpdate method can be used to perform comparisons between the props passed in to the 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 GoJS 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 !== null) { diagram.div = null; if (this.modelChangedListener !== null) diagram.removeModelChangedListener(this.modelChangedListener); } }