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:

You can of course use Redux or another state management library if you prefer that to typical React state and props.

  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);
    }
  }