React Sigma.js: The Practical Guide to Interactive Graph Visualization in React
Why React Sigma.js Deserves Your Attention
Graph visualization in the browser has always been a compromise.
SVG-based libraries like D3.js give you incredible flexibility but buckle under the weight of a few thousand nodes.
Canvas-based tools are fast but feel like assembling furniture with a screwdriver and no instructions.
Sigma.js found the sweet spot — WebGL rendering, clean API, and genuine scalability —
and React Sigma.js wraps all of that into a set of idiomatic React components that actually make sense.
The library — officially distributed as @react-sigma/core — is not a toy wrapper.
It gives you full access to the Sigma.js instance via React hooks, a composable component architecture,
and first-class integration with Graphology,
which handles your graph data model (nodes, edges, attributes) as a proper JavaScript object.
This combination means your graph is not just rendered — it is managed.
If you are building a knowledge graph explorer, a network topology dashboard, a social graph,
or any kind of interactive node-link diagram in React, this is likely the most production-ready option available in 2024–2025.
Let’s go through everything from zero to a working, customized, plugin-enhanced graph —
with no hand-waving and no “left as an exercise to the reader.”
Understanding the Architecture Before You Install Anything
Before running npm install, it pays to understand how the pieces fit together,
because react-sigmajs operates on a three-layer model and conflating those layers is
the fastest way to write code you will hate in two weeks.
Layer 1 — Graphology: This is your data layer.
Graphology is a robust, multipurpose graph object for JavaScript.
It stores nodes and edges with arbitrary attributes, supports directed and undirected graphs,
and offers a rich ecosystem of utility libraries — including layout algorithms,
metrics (betweenness centrality, PageRank), and import/export formats like GEXF and GraphML.
You create your graph here. Everything else just reads from it.
Layer 2 — Sigma.js: This is the rendering engine.
Sigma.js reads your Graphology instance and renders it to a WebGL canvas.
It handles camera transformations, zoom, pan, and node/edge rendering programs.
It also fires the raw DOM events you need for interactivity.
You rarely interact with Sigma.js directly in React — instead, you access it via the useSigma() hook.
Layer 3 — @react-sigma/core: This is the React integration layer.
It provides <SigmaContainer> as the root component that initializes Sigma and provides context,
plus hooks like useSigma(), useLoadGraph(), and useRegisterEvents()
that let you interact with the graph reactively.
Child components mounted inside SigmaContainer have access to this context,
which is how you load data, respond to events, and apply layouts — all as React components.
Installation and Project Setup
Getting react-sigmajs running
requires three packages: the Sigma.js core, Graphology, and the React wrapper itself.
This is a deliberate design choice — the library does not bundle its dependencies,
which keeps version control in your hands and avoids the “my package has its own version of Sigma” class of bugs.
# Install core dependencies
npm install sigma graphology @react-sigma/core
# Optional but highly recommended for layouts
npm install graphology-layout graphology-layout-forceatlas2
# Optional for data import
npm install graphology-gexf graphology-graphml
Once installed, your entry point is <SigmaContainer>.
It accepts a style prop for dimensions (it will take 100% of its parent by default),
a settings prop for Sigma configuration, and an optional graph prop
if you want to pass a pre-built Graphology instance directly.
Everything that needs to interact with the graph must be a child of this container —
that is the entire mental model.
Here is a minimal working example.
No Lorem Ipsum nodes, no placeholder data —
this actually renders a small directed graph with labeled nodes:
// GraphComponent.tsx
import { useEffect } from "react";
import { SigmaContainer, useLoadGraph, useRegisterEvents } from "@react-sigma/core";
import "@react-sigma/core/lib/react-sigma.min.css";
import Graph from "graphology";
// Child component: loads the graph data into Sigma's context
function LoadGraph() {
const loadGraph = useLoadGraph();
useEffect(() => {
const graph = new Graph({ type: "directed" });
graph.addNode("react", {
label: "React",
x: 0,
y: 0,
size: 20,
color: "#61DAFB",
});
graph.addNode("sigma", {
label: "Sigma.js",
x: 1,
y: 1,
size: 18,
color: "#6c63ff",
});
graph.addNode("graphology", {
label: "Graphology",
x: -1,
y: 1,
size: 15,
color: "#f97316",
});
graph.addEdge("react", "sigma", { size: 2, color: "#999" });
graph.addEdge("react", "graphology", { size: 2, color: "#999" });
graph.addEdge("sigma", "graphology", { size: 1, color: "#ccc" });
loadGraph(graph);
}, [loadGraph]);
return null;
}
// Root export: SigmaContainer wraps everything
export default function GraphComponent() {
return (
<SigmaContainer
style={{ height: "600px", width: "100%" }}
settings={{
renderEdgeLabels: true,
defaultEdgeType: "arrow",
labelDensity: 0.07,
labelGridCellSize: 60,
labelRenderedSizeThreshold: 8,
}}
>
<LoadGraph />
</SigmaContainer>
);
}
Note the import of the CSS file — this is easy to forget and will result in a container with zero dimensions.
Also note that x and y attributes on nodes are mandatory when you are not using a layout algorithm.
Sigma is not a layout engine; it is a renderer. Position is your responsibility —
unless you delegate it to a layout algorithm, which we will cover next.
Applying Layouts: From Static Positions to Force-Directed Magic
Hand-coding node coordinates is fine for three nodes.
For anything larger — a social network, a dependency graph, an org chart —
you need a layout algorithm.
The most popular in the Graphology ecosystem is ForceAtlas2,
a force-directed algorithm originally designed for Gephi
that tends to produce human-readable clustered layouts even for messy real-world data.
For smaller graphs (under ~500 nodes), you can run ForceAtlas2 synchronously before loading the graph.
For anything larger, use the Web Worker version — graphology-layout-forceatlas2/worker —
which runs the simulation off the main thread and lets you stream the result back.
The @react-sigma/layout-forceatlas2 package wraps this into a React hook
that handles the worker lifecycle for you:
npm install @react-sigma/layout-forceatlas2
import {
useWorkerLayoutForceAtlas2,
ControlsContainer,
ZoomControl,
FullScreenControl,
} from "@react-sigma/layout-forceatlas2";
function LayoutController() {
const { start, stop, isRunning } = useWorkerLayoutForceAtlas2({
settings: { slowDown: 10 },
});
useEffect(() => {
// Start the layout on mount, stop after 5 seconds
start();
const timer = setTimeout(stop, 5000);
return () => {
clearTimeout(timer);
stop();
};
}, [start, stop]);
return null;
}
// Use inside SigmaContainer alongside LoadGraph
<SigmaContainer ...>
<LoadGraph />
<LayoutController />
</SigmaContainer>
Other available layout algorithms include graphology-layout‘s circular, random,
and rotation utilities, as well as graphology-layout-noverlap for post-processing
to prevent nodes from overlapping after the primary layout is done.
Real-world graphs often benefit from running ForceAtlas2 first, then noverlap as a cleanup pass —
the result is far more legible than either algorithm alone.
Handling Events and Adding Interactivity
A static graph is a picture.
An interactive graph is a tool.
The useRegisterEvents() hook is how you cross that line.
It accepts a map of Sigma event names to handler functions and registers them against the Sigma instance
in the current context — cleaning up automatically when the component unmounts.
Sigma fires events at both the graph level (clickNode, enterNode, leaveNode,
clickEdge, clickStage) and the camera level (cameraUpdated).
The node events give you the node key, which you then use to read attributes from your Graphology instance
via the useSigma() hook:
import { useEffect, useState } from "react";
import { useSigma, useRegisterEvents } from "@react-sigma/core";
function GraphEvents({ onNodeClick }) {
const sigma = useSigma();
const registerEvents = useRegisterEvents();
const [hoveredNode, setHoveredNode] = useState(null);
useEffect(() => {
registerEvents({
clickNode: (event) => {
const nodeData = sigma.getGraph().getNodeAttributes(event.node);
onNodeClick(nodeData);
},
enterNode: (event) => {
setHoveredNode(event.node);
// Highlight the node by updating its color attribute
sigma.getGraph().setNodeAttribute(event.node, "color", "#ff6b6b");
sigma.refresh();
},
leaveNode: (event) => {
setHoveredNode(null);
sigma.getGraph().setNodeAttribute(event.node, "color", "#6c63ff");
sigma.refresh();
},
clickStage: () => {
// Deselect everything when clicking empty space
onNodeClick(null);
},
});
}, [registerEvents, sigma, onNodeClick]);
return null;
}
One subtlety worth knowing: sigma.refresh() tells Sigma to re-read node attributes from Graphology
and re-render.
It is lightweight compared to a full re-render cycle.
However, if you are making structural changes to the graph (adding or removing nodes/edges),
you need sigma.refresh({ skipIndexation: false }) — or simply call sigma.refresh()
with no arguments, which triggers full re-indexation.
The distinction matters at scale: unnecessary full re-indexation on large graphs is a performance sink.
Customization: Nodes, Edges, and Rendering Programs
React Sigma.js customization
operates at two levels.
The first level is attribute-based: you set color, size, label,
and type directly on your Graphology nodes and edges, and Sigma reads them at render time.
This covers the vast majority of use cases and requires no custom code beyond setting the right attributes.
The second level is program-based: for truly custom rendering — bordered nodes, icon nodes,
gradient edges, or anything that deviates from Sigma’s built-in shapes —
you write or import a custom node program or edge program.
These are WebGL shader programs that define how a node or edge is drawn to the canvas.
Sigma.js ships with @sigma/node-border, @sigma/node-image,
and @sigma/edge-curve as ready-made extensions:
npm install @sigma/node-border @sigma/node-image @sigma/edge-curve
import { NodeBorderProgram } from "@sigma/node-border";
import { NodeImageProgram } from "@sigma/node-image";
import { EdgeCurveProgram } from "@sigma/edge-curve";
<SigmaContainer
settings={{
nodeProgramClasses: {
border: NodeBorderProgram,
image: NodeImageProgram,
},
edgeProgramClasses: {
curved: EdgeCurveProgram,
},
defaultNodeType: "border",
defaultEdgeType: "curved",
}}
>
<LoadGraph />
</SigmaContainer>
When using NodeImageProgram, set the image attribute on each node to a URL —
Sigma will fetch and cache the image, then render it inside the node circle.
This is particularly useful for social graphs where nodes represent users with avatars,
or knowledge graphs where nodes represent typed entities with icons.
The NodeBorderProgram lets you add a colored border (useful for selection states)
via borderColor and borderSize node attributes.
Working with Plugins and the @react-sigma Ecosystem
The @react-sigma namespace on npm is the official home of the plugin ecosystem.
Beyond the core and layout packages, it includes UI controls, search utilities, and camera helpers
that save significant boilerplate.
The most commonly used are @react-sigma/controls (zoom in/out, full-screen, reset camera buttons),
and layout packages for ForceAtlas2, Circular, and Random layouts.
The controls package deserves special mention because graph UX without camera controls
is a frustrating experience for end users.
ControlsContainer renders a positioned overlay;
inside it you drop ZoomControl and FullScreenControl.
That is genuinely all there is to it:
import {
ControlsContainer,
ZoomControl,
FullScreenControl,
SearchControl,
} from "@react-sigma/core";
<SigmaContainer style={{ height: "600px", width: "100%" }}>
<LoadGraph />
<ControlsContainer position="bottom-right">
<ZoomControl />
<FullScreenControl />
</ControlsContainer>
<ControlsContainer position="top-right">
<SearchControl style={{ width: "200px" }} />
</ControlsContainer>
</SigmaContainer>
SearchControl ships with built-in node label search, keyboard navigation,
and camera animation to the selected node.
For most graph exploration UIs, this single component replaces several hundred lines of custom search code.
If you need more control over the search behavior — fuzzy matching, filtering by node type,
searching edge labels — you can build on top of the useSigma() hook
and Graphology’s filterNodes() method to roll your own,
but the built-in control is a solid starting point.
Loading Real Data: GEXF, GraphML, and JSON
Toy examples with hand-coded nodes are fine for tutorials.
Production graphs come from APIs, databases, or exported files.
The most common interchange formats in the graph world are GEXF (used by Gephi),
GraphML (used by yEd and many academic tools), and plain JSON.
Graphology has dedicated parsers for all three.
import { parse as parseGexf } from "graphology-gexf";
import { parse as parseGraphml } from "graphology-graphml";
import Graph from "graphology";
// Load from GEXF string (e.g., fetched from an API)
async function loadFromGexf(url) {
const response = await fetch(url);
const gexfString = await response.text();
const graph = parseGexf(Graph, gexfString);
return graph;
}
// Load from JSON adjacency data (custom API response)
function loadFromJson(nodes, edges) {
const graph = new Graph({ type: "undirected" });
nodes.forEach(({ id, label, ...attrs }) => {
graph.addNode(id, { label, ...attrs });
});
edges.forEach(({ source, target, ...attrs }) => {
graph.addEdge(source, target, attrs);
});
return graph;
}
When loading data from an API in a React component,
combine useLoadGraph() with a useEffect that fires on mount.
If you need to reload the graph when user filters change,
include those filters in the dependency array and call loadGraph with the new graph —
it replaces the current graph entirely and triggers a Sigma re-render.
This is simpler than trying to do incremental updates for most use cases.
For graphs larger than ~50,000 nodes, consider server-side pre-computation of layouts
and shipping node positions as part of the data payload.
Running ForceAtlas2 client-side on 100K nodes, even in a Web Worker,
is going to make your users wait and your browser tab sweat.
Gephi, NetworkX (Python), or even a simple ForceAtlas2 run in Node.js
can pre-compute positions that you store alongside your data.
Performance Optimization for Large-Scale React Network Graphs
React Sigma.js is fast by default because WebGL is fast.
But “fast by default” does not mean “immune to poor decisions.”
There are several patterns that consistently cause performance issues in production graph applications,
and avoiding them is mostly a matter of knowing they exist.
The first one is calling sigma.refresh() inside React state update cycles without debouncing.
Every time you update a node attribute in response to a hover or click event,
you should batch updates and call refresh once — not once per node attribute update.
For example, if you are changing five attributes on a node (color, size, borderColor, label, type),
make all five setNodeAttribute calls before calling refresh().
The second pattern to avoid is storing graph data in React state.
Your Graphology graph object is not React state — it is mutable graph data managed by Sigma.
Wrapping it in useState triggers unnecessary re-renders every time the graph changes.
Use a ref (useRef) if you need to hold a reference to the graph outside of a Sigma context,
or simply access it via sigma.getGraph() inside child components.
Keep React state for UI-level concerns (selected node ID, sidebar visibility, filter values)
and Graphology attributes for graph-level concerns (node color, size, visibility).
The rule is simple: if it affects what the graph looks like,
it belongs in Graphology attributes. If it affects what your UI looks like,
it belongs in React state. Mixing the two is where performance bugs are born.
TypeScript Support and Type Safety
React Sigma.js
is written in TypeScript and ships full type definitions.
More usefully, Graphology supports typed graphs —
you can parameterize your Graph instance with node attribute and edge attribute types,
getting full autocomplete and type checking throughout your graph code:
import Graph from "graphology";
// Define your node and edge attribute shapes
interface NodeAttributes {
label: string;
x: number;
y: number;
size: number;
color: string;
type?: "person" | "organization" | "topic";
image?: string;
}
interface EdgeAttributes {
size?: number;
color?: string;
weight?: number;
type?: "directed" | "curved";
}
// Typed Graphology graph
const graph = new Graph<NodeAttributes, EdgeAttributes>({ type: "directed" });
// Now addNode enforces NodeAttributes
graph.addNode("node1", {
label: "Alice",
x: 0,
y: 0,
size: 15,
color: "#6c63ff",
type: "person",
});
// TypeScript will error if you typo an attribute name
// graph.addNode("node2", { lable: "Bob", ... }); // ❌ Error: 'lable' does not exist
Sigma’s settings prop, all hook return values, and event payloads are fully typed.
If you are using a modern IDE like VS Code or WebStorm,
you will get inline documentation and autocomplete for all Sigma settings —
which is genuinely useful given how many configuration options exist.
No need to keep the docs open in a separate tab.
One TypeScript nuance: when you use useSigma(),
the returned Sigma instance is typed as Sigma<NodeAttributes, EdgeAttributes>
only if you declare those types when rendering SigmaContainer.
If you are getting any back from sigma.getGraph().getNodeAttributes(),
check that your graph type parameters are flowing through the component tree correctly.
React Sigma.js vs. the Alternatives: An Honest Comparison
Before committing to any React graph library,
it is worth knowing what you are trading away as well as what you are getting.
The main contenders in the React ecosystem are Cytoscape.js, vis.js / vis-network,
D3.js, and react-force-graph.
- Cytoscape.js — feature-rich, excellent for biological and scientific networks,
strong layout algorithm support, but SVG/Canvas rendering means performance degrades at ~10K nodes.
The React wrapper (react-cytoscapejs) is a thin layer that feels un-React-like. - vis-network — good out-of-the-box defaults, easy to use,
but the codebase is large, maintenance has been inconsistent,
and the API is not designed around the React component model. - D3.js — the gold standard for data visualization,
but integrating D3’s mutation-heavy DOM model with React’s virtual DOM is an ongoing fight.
At scale, D3 SVG graphs become slideshows. - react-force-graph — excellent for 3D force graphs and quick prototypes,
WebGL-based, but limited customization compared to Sigma + Graphology.
React Sigma.js wins on the combination of WebGL performance,
React-native API design, TypeScript support,
and the Graphology ecosystem for data manipulation.
It loses if you need highly specialized layouts out of the box (Cytoscape has more),
or if you need 3D rendering (use react-force-graph).
For 2D interactive network graphs in a React application that needs to scale,
it is the most sensible default choice available today.
Frequently Asked Questions
How do I install and set up react-sigmajs in a React project?
Run npm install sigma graphology @react-sigma/core to install the three core packages.
Import the CSS file with import "@react-sigma/core/lib/react-sigma.min.css" —
this is required for the container to have dimensions.
Wrap your graph in <SigmaContainer>, and load your Graphology graph inside a child component
using the useLoadGraph() hook inside a useEffect.
Make sure every node has x, y, and size attributes,
or apply a layout algorithm before loading.
Can react-sigmajs handle large-scale graph datasets?
Yes — WebGL rendering allows Sigma.js to handle tens of thousands of nodes and edges
with smooth interactivity, far beyond what SVG-based libraries can manage.
For graphs over ~50,000 nodes, pre-compute layouts server-side and ship positions with the data.
Client-side, use the Web Worker version of ForceAtlas2
(graphology-layout-forceatlas2/worker or @react-sigma/layout-forceatlas2)
to avoid blocking the main thread during layout computation.
Avoid storing graph data in React state — use Graphology attributes and
sigma.refresh() for graph-level updates.
How do I customize nodes and edges in react-sigmajs?
Basic customization — color, size, label, type — is done through Graphology node and edge attributes.
Set them when building your graph or update them dynamically with setNodeAttribute()
followed by sigma.refresh().
For custom shapes and rendering — bordered nodes, image nodes, curved edges —
install extension packages like @sigma/node-border, @sigma/node-image,
or @sigma/edge-curve, then register them in the nodeProgramClasses
or edgeProgramClasses settings of SigmaContainer.
Set the type attribute on individual nodes/edges to select which program renders them.
Wrapping Up
React Sigma.js is not the flashiest graph library in the ecosystem,
but it might be the most serious one.
The combination of WebGL rendering, a proper graph data model via Graphology,
idiomatic React hooks, and a growing plugin ecosystem makes it a strong choice
for any application where graph visualization is a feature — not just a prototype.
The learning curve is real but reasonable:
the mental model of “Graphology manages data, Sigma renders it, React components coordinate both”
clicks quickly, and once it does, everything from dynamic filtering to real-time graph updates
follows naturally from the same pattern.
The TypeScript support is excellent, the documentation has improved significantly in recent versions,
and the community around Sigma.js and Graphology is active.
If you are building anything from a dependency graph explorer to a fraud detection network dashboard,
this is a stack worth investing in.
The full working example from this guide is available on
Dev.to via StackForgeDev —
fork it, break it, and build something with it.
