Skip to content
  1. Tutorials

Ogma with React Hooks

The following tutorial will show how to create a Ogma component using React hooks.

You may also want to check out the ogma-react wrapper for seamless integration between Ogma and React.

The component is called OgmaWrapper and it can be customised to handle different events such onNodeClick as in the tutorial.

As first step the component should create a Ogma instance with useRef, to hold it for the whole component life, and useEffect to initialize the instance:

jsx
import React, { useRef, useEffect } from 'react';
import Ogma from '@linkurious/ogma';

function OgmaWrapper({ graph }) {
  const containerId = 'ogma-container';

  // initialize ogma and get a reference to it
  const ogmaRef = useRef(new Ogma());

  // note the second parameter here ( [] ) which makes it run only once until unmounted
  useEffect(() => {
    ogmaRef.current.setContainer(containerId);
  }, []);

  // setup events and more

  // do something when the graph changes

  return (
    <div className="OgmaWrapper">
      <div id={containerId} />
    </div>
  );
}

Basic lifecycle hooks

At this point ogmaRef.current contains the Ogma instance and it is possible to use it to bind events and more setup. Note that the second parameter of useEffect here is an empty array ([]), so that the effect will be called once in the component lifetime.

In this example it is shown how it is possible to make the React OgmaWrapper comunicate with its parents using a onNodeClick function prop:

jsx
import React, { useRef, useEffect } from 'react';
import Ogma from '@linkurious/ogma';

function OgmaWrapper({ graph, onNodeClick }) {
  const containerId = 'ogma-container';

  // initialize ogma and get a reference to it
  const ogmaRef = useRef(new Ogma());

  // note the second parameter here ( [] ) which makes it run only once until unmounted
  useEffect(() => {
    ogmaRef.current.setContainer(containerId);
  }, []);

  // setup events and more
  useEffect(() => {
    const clickHandler = ({ target }) => {
      if (target && target.isNode) {
        onNodeClick(target.getId());
      } else {
        onNodeClick(null);
      }
    };

    ogmaRef.current.events.on('click', clickHandler);

    return () => {
      ogmaRef.current.events.off(clickHandler);
    };
  }, [onNodeClick]);

  // do something when the graph changes

  return (
    <div className="OgmaWrapper">
      <div id={containerId} />
    </div>
  );
}

Notice how the second hook created has an array now contains the onNodeClick function: it is a best practice to put as useEffect dependency all external functions in the array. It is also a good practice to provide a function to execute when the useEffect is destroyed before updated, as in this case remove the event listener.

Now the App knows when a click happens within the OgmaWrapper. But what if the App changes the graph data? The following shows how to use useEffect to handle the graph data changes:

jsx
import React, { useRef, useEffect } from 'react';
import Ogma from '@linkurious/ogma';

function OgmaWrapper({ graph, onNodeClick }) {
  const containerId = 'ogma-container';

  // initialize ogma and get a reference to it
  // note the second parameter here ( [] ) which makes it run only once until unmounted
  const ogmaRef = useRef(new Ogma());

  // note the second parameter here ( [] ) which makes it run only once until unmounted
  useEffect(() => {
    ogmaRef.current.setContainer(containerId);
  }, []);

  useEffect(() => {
    const clickHandler = ({ target }) => {
      if (target && target.isNode) {
        onNodeClick(target.getId());
      } else {
        onNodeClick(null);
      }
    };

    ogmaRef.current.events.on('click', clickHandler);

    return () => {
      ogmaRef.current.events.off(clickHandler);
    };
  }, [onNodeClick]);

  // setup events and more
  useEffect(() => {
    ogmaRef.current.events.on('click', ({ target }) => {
      if (target && target.isNode) {
        onNodeClick(target.getId());
      } else {
        onNodeClick(null);
      }
    });
  }, []);

  // do something when the graph changes
  useEffect(() => {
    // clear the current graph shown and load the new data
    ogmaRef.current.clearGraph();
    ogmaRef.current
      .setGraph(graph)
      .then(() =>
        ogmaRef.current.layouts.forceLink({ locate: graph.nodes.length !== 0 })
      );
  }, [graph]);

  return (
    <div className="OgmaWrapper">
      <div id={containerId} />
    </div>
  );
}

Now the chart will be updated at every node added or removed, performing a new layout every time. But what about comunicating to the parent component about the new positions or more in general the new state of the chart after the change?

jsx
import React, { useRef, useEffect } from 'react';
import Ogma from '@linkurious/ogma';

function OgmaWrapper({ graph, onNodeClick }) {
  const containerId = 'ogma-container';

  // initialize ogma and get a reference to it
  const ogmaRef = useRef(new Ogma());

  // note the second parameter here ( [] ) which makes it run only once until unmounted
  useEffect(() => {
    ogmaRef.current.setContainer(containerId);
  }, []);

  // setup events and more
  useEffect(() => {
    const clickHandler = ({ target }) => {
      if (target && target.isNode) {
        onNodeClick(target.getId());
      } else {
        onNodeClick(null);
      }
    };

    ogmaRef.current.events.on('click', clickHandler);

    return () => {
      ogmaRef.current.events.off(clickHandler);
    };
  }, [onNodeClick]);

  // do something when on change: execute the useEffect only when the graph data changes
  useEffect(() => {
    // clear the current graph shown and load the new data
    ogmaRef.current.clearGraph();
    ogmaRef.current
      .setGraph(graph)
      .then(() =>
        ogmaRef.current.layouts.forceLink({ locate: graph.nodes.length !== 0 })
      )
      .then(() => {
        graph.nodes = ogmaRef.current.getNodes().toJSON();
        graph.edges = ogmaRef.current.getEdges().toJSON();
      });
  }, [graph]);

  return (
    <div className="OgmaWrapper">
      <div id={containerId} />
    </div>
  );
}

Wrapping the component

At this point it is possible to create a App component that will use this OgmaWrapper component:

jsx
import React, { useState } from 'react';
import OgmaWrapper from './OgmaWrapper';

import './App.css';

export default function App() {
  const [graph, setGraph] = useState({
    nodes: [{ id: 0 }],
    edges: []
  });

  const [currentNode, setCurrentNode] = useState(null);

  function addNode() {
    if (graph.nodes.length === 0) {
      return setGraph({
        nodes: [{ id: 0 }],
        edges: []
      });
    }
    const lastNode = graph.nodes[graph.nodes.length - 1];
    setGraph({
      nodes: [
        ...graph.nodes,
        {
          id: graph.nodes.length
        }
      ],
      edges: [
        ...graph.edges,
        { source: lastNode.id, target: graph.nodes.length }
      ]
    });
  }

  function removeNode() {
    const r = graph.nodes.pop();
    setGraph({
      nodes: [...graph.nodes],
      edges: [
        ...graph.edges.filter(e => e.source !== r.id && e.target !== r.id)
      ]
    });
  }

  return (
    <div className="App">
      <section className="App-vis">
        <OgmaWrapper graph={graph} onNodeClick={setCurrentNode} />
        <div className="App-data">
          <button onClick={addNode}>Add node</button>
          <button onClick={removeNode}>Remove node</button>
          <div>
            <div>
              Clicked node: {currentNode === null ? 'none' : currentNode}
            </div>
            <code>
              <pre>{JSON.stringify(graph, 0, 2)}</pre>
            </code>
          </div>
        </div>
      </section>
    </div>
  );
}

Callbacks

Mind that setCurrentNode in this case comes from a useState hooks, therefore it will be stable (not replaced at every rendering) for the lifetime of the App component. In case you need to set a specific function for the onNodeClick prop, we recommend to use the useCallback to ensure better handling of the rendering performance. For instance:

jsx
import React, { useState, useCallback } from "react";
import OgmaWrapper from "./OgmaWrapper";

import './App.css';

export default function App () {
  const [graph, setGraph] = useState({
    nodes: [{id: 0}],
    edges: []
  });

  const [currentNode, setCurrentNode] = useState(null);

  // in this example we update the selection but also operate some action on the graph,
  // therefore graph will be put as dependency. This enables useCallback to memoize the handler until
  // the graph variable is changed.
  const doSomethingComplex = useCallback( (nodeId) => {
    setCurrentNode(nodeId);
    // do something to graph
    doSomething(graph);
  }, [graph]);

  ...
  return (
    <div className="App">
      <section className="App-vis">
        <OgmaWrapper graph={graph} onNodeClick={doSomethingComplex} />
        <div className="App-data">
          <button onClick={addNode}>Add node</button>
          <button onClick={removeNode}>Remove node</button>
          <div>
            <div>Clicked node: {currentNode === null ? 'none' : currentNode }</div>
            <code>
              <pre>{JSON.stringify(graph, 0, 2)}</pre>
            </code>
          </div>
        </div>
      </section>
    </div>
  );
}