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:

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