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.

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, including the various props it takes, can be found on the Github or NPM pages. Our examples will be using a GraphLinksModel, but any model can be used.

Quick start with an existing React application

Installation

Start by installing GoJS and gojs-react: npm install gojs gojs-react.

Diagram styling

Next, set up a CSS class for the GoJS diagram's div:


  /* App.css */
  .diagram-component {
    width: 400px;
    height: 400px;
    border: solid 1px black;
    background-color: white;
  }

Rendering the component

Finally, add an initDiagram function and a model change handler function, and add the ReactDiagram component inside your render method. Note that the UndoManager should always be enabled to allow for transactions to take place, but the UndoManager.maxHistoryLength can be set to 0 to prevent undo and redo.


  // App.js
  import React from 'react';

  import * as go from 'gojs';
  import { ReactDiagram } from 'gojs-react';

  import './App.css';  // contains .diagram-component CSS

  // ...

  /**
   * Diagram initialization method, which is passed to the ReactDiagram component.
   * This method is responsible for making the diagram and initializing the model and any templates.
   * The model's data should not be set here, as the ReactDiagram component handles that via the other props.
   */
  function initDiagram() {
    const $ = go.GraphObject.make;
    // set your license key here before creating the diagram: go.Diagram.licenseKey = "...";
    const diagram =
      $(go.Diagram,
        {
          'undoManager.isEnabled': true,  // must be set to allow for model change listening
          // 'undoManager.maxHistoryLength': 0,  // uncomment disable undo/redo functionality
          'clickCreatingTool.archetypeNodeData': { text: 'new node', color: 'lightblue' },
          model: new go.GraphLinksModel(
            {
              linkKeyProperty: 'key'  // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel
            })
        });

    // define a simple Node template
    diagram.nodeTemplate =
      $(go.Node, 'Auto',  // the Shape will go around the TextBlock
        new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify),
        $(go.Shape, 'RoundedRectangle',
          { name: 'SHAPE', fill: 'white', strokeWidth: 0 },
          // Shape.fill is bound to Node.data.color
          new go.Binding('fill', 'color')),
        $(go.TextBlock,
          { margin: 8, editable: true },  // some room around the text
          new go.Binding('text').makeTwoWay()
        )
      );

    return diagram;
  }

  /**
   * This function handles any changes to the GoJS model.
   * It is here that you would make any updates to your React state, which is dicussed below.
   */
  function handleModelChange(changes) {
    alert('GoJS model changed!');
  }

  // render function...
  function App() {
    return (
      <div>
        ...
        <ReactDiagram
          initDiagram={initDiagram}
          divClassName='diagram-component'
          nodeDataArray={[
            { key: 0, text: 'Alpha', color: 'lightblue', loc: '0 0' },
            { key: 1, text: 'Beta', color: 'orange', loc: '150 0' },
            { key: 2, text: 'Gamma', color: 'lightgreen', loc: '0 150' },
            { key: 3, text: 'Delta', color: 'pink', loc: '150 150' }
          ]}
          linkDataArray={[
            { key: -1, from: 0, to: 1 },
            { key: -2, from: 0, to: 2 },
            { key: -3, from: 1, to: 1 },
            { key: -4, from: 2, to: 3 },
            { key: -5, from: 3, to: 0 }
          ]}
          onModelChange={handleModelChange}
        />
        ...
      </div>
    );
  }

That's it! You should now have a GoJS diagram rendering within your React application. Try editing the text of a node or deleting a node, and you'll see an alert on the page.

Usage in a stateful React app

Typically the data being passed to the ReactDiagram component will be used elsewhere in your app and will exist in React state. For example, you may have some kind of inspector that can be used to modify node properties, and therefore the state should be lifted up and held by a parent component of both the diagram and the inspector.

A basic setup can be seen in the gojs-react-basic project, but we'll describe some of the methodology here.

Creating a wrapper component

When handling state, it is often useful to write a wrapper component around the gojs-react components to pass the necessary props along and keep GoJS initialization out of the main app. There are a few things that should be set up in the wrapper component:

Node and link data are merged into the GoJS model, thus properties should not be removed from node or link data, but rather set to undefined if they are no longer needed; GoJS avoids destructive merging.

Below, we'll pass linkDataArray and modelData as props to the ReactDiagram, but note that they are not always needed in gojs-react components, so your app may not need to include them. For proper initial loading of data, one should have the data ready before the ReactDiagram component mounts. This allows layouts and linking to occur properly with the initial data set.


  import * as go from 'gojs';
  import { ReactDiagram } from 'gojs-react';
  import * as React from 'react';

  // props passed in from a parent component holding state, some of which will be passed to ReactDiagram
  interface WrapperProps {
    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<WrapperProps, {}> {
    /**
     * Ref to keep a reference to the component, which provides access to the GoJS diagram via getDiagram().
     */
    private diagramRef: React.RefObject<ReactDiagram>;

    constructor(props: WrapperProps) {
      super(props);
      this.diagramRef = React.createRef();
    }

    /**
     * Get the diagram reference and add any desired diagram listeners.
     * Typically the same function will be used for each listener,
     * with the function using a switch statement to handle the events.
     * This is only necessary when you want to define additional app-specific diagram listeners.
     */
    public componentDidMount() {
      if (!this.diagramRef.current) return;
      const diagram = this.diagramRef.current.getDiagram();
      if (diagram instanceof go.Diagram) {
        diagram.addDiagramListener('ChangedSelection', this.props.onDiagramEvent);
      }
    }

    /**
     * Get the diagram reference and remove listeners that were added during mounting.
     * This is only necessary when you have defined additional app-specific diagram listeners.
     */
    public componentWillUnmount() {
      if (!this.diagramRef.current) return;
      const diagram = this.diagramRef.current.getDiagram();
      if (diagram instanceof go.Diagram) {
        diagram.removeDiagramListener('ChangedSelection', this.props.onDiagramEvent);
      }
    }

    /**
     * Diagram initialization method, which is passed to the ReactDiagram component.
     * This method is responsible for making the diagram and initializing the model, any templates,
     * and maybe doing other initialization tasks like customizing tools.
     * The model's data should not be set here, as the ReactDiagram component handles that via the other props.
     */
    private initDiagram(): go.Diagram {
      const $ = go.GraphObject.make;
      // set your license key here before creating the diagram: go.Diagram.licenseKey = "...";
      const diagram =
        $(go.Diagram,
          {
            'undoManager.isEnabled': true,  // must be set to allow for model change listening
            // 'undoManager.maxHistoryLength': 0,  // uncomment disable undo/redo functionality
            'clickCreatingTool.archetypeNodeData': { text: 'new node', color: 'lightblue' },
            model: new go.GraphLinksModel(
              {
                linkKeyProperty: 'key',  // IMPORTANT! must be defined for merges and data sync when using GraphLinksModel
                // positive keys for nodes
                makeUniqueKeyFunction: (m: go.Model, data: any) => {
                  let k = data.key || 1;
                  while (m.findNodeDataForKey(k)) k++;
                  data.key = k;
                  return k;
                },
                // negative keys for links
                makeUniqueLinkKeyFunction: (m: go.GraphLinksModel, data: any) => {
                  let k = data.key || -1;
                  while (m.findLinkDataForKey(k)) k--;
                  data.key = k;
                  return k;
                }
              })
          });

      // define a simple Node template
      diagram.nodeTemplate =
        $(go.Node, 'Auto',  // the Shape will go around the TextBlock
          new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify),
          $(go.Shape, 'RoundedRectangle',
            {
              name: 'SHAPE', fill: 'white', strokeWidth: 0,
              // set the port properties:
              portId: '', fromLinkable: true, toLinkable: true, cursor: 'pointer'
            },
            // Shape.fill is bound to Node.data.color
            new go.Binding('fill', 'color')),
          $(go.TextBlock,
            { margin: 8, editable: true, font: '400 .875rem Roboto, sans-serif' },  // some room around the text
            new go.Binding('text').makeTwoWay()
          )
        );

      // relinking depends on modelData
      diagram.linkTemplate =
        $(go.Link,
          new go.Binding('relinkableFrom', 'canRelink').ofModel(),
          new go.Binding('relinkableTo', 'canRelink').ofModel(),
          $(go.Shape),
          $(go.Shape, { toArrow: 'Standard' })
        );

      return diagram;
    }

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

Using the wrapper component within the app

The application should set up a few things to be passed to the wrapper described above:


  import * as go from 'gojs';
  import * as React from 'react';

  import { DiagramWrapper } from './components/Diagram';

  interface AppState {
    // ...
    nodeDataArray: Array<go.ObjectData>;
    linkDataArray: Array<go.ObjectData>;
    modelData: go.ObjectData;
    selectedKey: number | null;
    skipsDiagramUpdate: boolean;
  }

  class App extends React.Component<{}, AppState> {
    constructor(props: object) {
      super(props);
      this.state = {
        // ...
        nodeDataArray: [
          { key: 0, text: 'Alpha', color: 'lightblue', loc: '0 0' },
          { key: 1, text: 'Beta', color: 'orange', loc: '150 0' },
          { key: 2, text: 'Gamma', color: 'lightgreen', loc: '0 150' },
          { key: 3, text: 'Delta', color: 'pink', loc: '150 150' }
        ],
        linkDataArray: [
          { key: -1, from: 0, to: 1 },
          { key: -2, from: 0, to: 2 },
          { key: -3, from: 1, to: 1 },
          { key: -4, from: 2, to: 3 },
          { key: -5, from: 3, to: 0 }
        ],
        modelData: {
          canRelink: true
        },
        selectedKey: null,
        skipsDiagramUpdate: false
      };
      // bind handler methods
      this.handleDiagramEvent = this.handleDiagramEvent.bind(this);
      this.handleModelChange = this.handleModelChange.bind(this);
      this.handleRelinkChange = this.handleRelinkChange.bind(this);
    }

    /**
     * Handle any app-specific DiagramEvents, in this case just selection changes.
     * On ChangedSelection, find the corresponding data and set the selectedKey state.
     *
     * This is not required, and is only needed when handling DiagramEvents from the GoJS diagram.
     * @param e a GoJS DiagramEvent
     */
    public handleDiagramEvent(e: go.DiagramEvent) {
      const name = e.name;
      switch (name) {
        case 'ChangedSelection': {
          const sel = e.subject.first();
          if (sel) {
            this.setState({ selectedKey: sel.key });
          } else {
            this.setState({ selectedKey: null });
          }
          break;
        }
        default: break;
      }
    }

    /**
     * Handle GoJS model changes, which output an object of data changes via Model.toIncrementalData.
     * This method should iterates over those changes and update state to keep in sync with the GoJS model.
     * This can be done via setState in React or another preferred state management method.
     * @param obj a JSON-formatted string
     */
    public handleModelChange(obj: go.IncrementalData) {
      const insertedNodeKeys = obj.insertedNodeKeys;
      const modifiedNodeData = obj.modifiedNodeData;
      const removedNodeKeys = obj.removedNodeKeys;
      const insertedLinkKeys = obj.insertedLinkKeys;
      const modifiedLinkData = obj.modifiedLinkData;
      const removedLinkKeys = obj.removedLinkKeys;
      const modifiedModelData = obj.modelData;

      console.log(obj);

      // see gojs-react-basic for an example model change handler
      // when setting state, be sure to set skipsDiagramUpdate: true since GoJS already has this update
    }

    /**
     * Handle changes to the checkbox on whether to allow relinking.
     * @param e a change event from the checkbox
     */
    public handleRelinkChange(e: any) {
      const target = e.target;
      const value = target.checked;
      this.setState({ modelData: { canRelink: value }, skipsDiagramUpdate: false });
    }

    public render() {
      let selKey;
      if (this.state.selectedKey !== null) {
        selKey = <p>Selected key: {this.state.selectedKey}</p>;
      }

      return (
        <div>
          <DiagramWrapper
            nodeDataArray={this.state.nodeDataArray}
            linkDataArray={this.state.linkDataArray}
            modelData={this.state.modelData}
            skipsDiagramUpdate={this.state.skipsDiagramUpdate}
            onDiagramEvent={this.handleDiagramEvent}
            onModelChange={this.handleModelChange}
          />
          <label>
            Allow Relinking?
            <input
              type='checkbox'
              id='relink'
              checked={this.state.modelData.canRelink}
              onChange={this.handleRelinkChange} />
          </label>
          {selKey}
        </div>
      );
    }
  }

A Note on Diagram Reinitialization

Occasionally you may want to treat a model update as if you were loading a completely new model. But initialization is done via the initDiagram function, within the componentDidMount lifecycle method, and only once. A regular model update is not treated as an initialization, so none of the initial... properties of your Diagram will apply.

To address this problem, ReactDiagram exposes a clear() method. When called, it clears its diagram of all nodes, links, and model data, and prepares the next state update to be treated as a diagram initialization. That will result in an initial layout and perform initial diagram content alignment and scaling. Note that the initDiagram function is not called again.

Here is a small sample of how one might trigger diagram reinitilization using the clear() method.


  public reinitModel() {
    this.diagramRef.current.clear();
    this.setState({
      nodeDataArray: [
        { key: 0, text: 'Epsilon', color: 'lightblue' },
        { key: 1, text: 'Zeta', color: 'orange' },
        { key: 2, text: 'Eta', color: 'lightgreen' },
        { key: 3, text: 'Theta', color: 'pink' }
      ],
      linkDataArray: [
        { key: -1, from: 0, to: 1 },
        { key: -2, from: 0, to: 2 },
        { key: -3, from: 1, to: 1 },
        { key: -4, from: 2, to: 3 },
        { key: -5, from: 3, to: 0 }
      ],
      skipsDiagramUpdate: false
    });
  }

These are the basics for setting up GoJS within a React application. See gojs-react-basic for a working example and the gojs-react Github page for further explanation of various props passed to the components.

GoJS