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