Skip to content

AnnotationsContextProvider ​

The AnnotationsContextProvider is a React Context Provider that manages all annotation state and provides methods to interact with annotations throughout your component tree.

Overview ​

The provider wraps your components and gives them access to:

  • The annotations editor (Control instance)
  • All annotations in the graph
  • Currently selected annotation
  • Style state for arrows and text
  • History management (undo/redo)
  • Drawing and annotation management methods

Basic Usage ​

Wrap your components with the provider inside the Ogma component:

tsx
import { Ogma } from "@linkurious/ogma-react";
import { AnnotationsContextProvider } from "@linkurious/ogma-annotations-react";

function App() {
  return (
    <Ogma graph={graph}>
      <AnnotationsContextProvider>
        <YourComponents />
      </AnnotationsContextProvider>
    </Ogma>
  );
}

Component Hierarchy

The AnnotationsContextProvider must be placed inside the Ogma component, as it depends on the Ogma instance.

Props ​

annotations (optional) ​

Load initial annotations when the provider mounts:

tsx
import { AnnotationCollection } from "@linkurious/ogma-annotations";

const initialAnnotations: AnnotationCollection = {
  type: "FeatureCollection",
  features: [
    // ... your annotations
  ]
};

<AnnotationsContextProvider annotations={initialAnnotations}>
  <YourComponents />
</AnnotationsContextProvider>;

This is useful when loading annotations from:

  • A database or API
  • LocalStorage
  • A file upload
  • Server-side rendering

children (required) ​

The React components that will have access to the annotations context:

tsx
<AnnotationsContextProvider>
  <Toolbar />
  <StylePanel />
  <AnnotationsList />
</AnnotationsContextProvider>

How It Works ​

Automatic Setup ​

When the provider mounts, it automatically:

  1. Creates a Control instance (the annotations editor)
  2. Attaches it to the Ogma instance
  3. Loads any initial annotations
  4. Sets up event listeners
  5. Manages internal state

Event Synchronization ​

The provider keeps React state synchronized with the annotations editor by listening to events:

typescript
// Simplified internal implementation
editor
  .on("add", () => {
    setAnnotations(editor.getAnnotations());
  })
  .on("remove", () => {
    setAnnotations(editor.getAnnotations());
  })
  .on("select", (selection) => {
    setCurrentAnnotation(selection);
    setArrowStyle(/* style from selected annotation */);
  })
  .on("history", () => {
    setCanUndo(editor.canUndo());
    setCanRedo(editor.canRedo());
  });

This means your React components automatically re-render when annotations change!

Automatic Cleanup ​

When the component unmounts, the provider automatically:

  • Destroys the Control instance
  • Removes all event listeners
  • Cleans up internal state

State Management ​

The provider manages several pieces of state:

Annotations State ​

tsx
const { annotations } = useAnnotationsContext();

console.log(annotations); // AnnotationCollection
console.log(annotations.features.length); // Number of annotations

Selection State ​

tsx
const { currentAnnotation, setCurrentAnnotation } = useAnnotationsContext();

if (currentAnnotation) {
  console.log("Selected:", currentAnnotation.id);
}

Style State ​

The provider tracks separate styles for arrows and text:

tsx
const { arrowStyle, setArrowStyle, textStyle, setTextStyle } =
  useAnnotationsContext();

// Update arrow style
setArrowStyle({
  ...arrowStyle,
  strokeColor: "#e74c3c"
});

// Update text style
setTextStyle({
  ...textStyle,
  fontSize: 18
});

Automatic Style Application

When you update the style via setArrowStyle or setTextStyle, the provider automatically applies the changes to the currently selected annotation!

History State ​

tsx
const { canUndo, canRedo, undo, redo } = useAnnotationsContext();

return (
  <>
    <button onClick={undo} disabled={!canUndo}>
      Undo
    </button>
    <button onClick={redo} disabled={!canRedo}>
      Redo
    </button>
  </>
);

Complete Example ​

Here's a full example showing the provider with multiple features:

tsx
import React from "react";
import { Ogma } from "@linkurious/ogma-react";
import { AnnotationsContextProvider } from "@linkurious/ogma-annotations-react";
import { AnnotationCollection } from "@linkurious/ogma-annotations";
import "@linkurious/ogma-annotations/style.css";

// Load annotations from storage
async function loadAnnotations(): Promise<AnnotationCollection> {
  const stored = localStorage.getItem("annotations");
  if (stored) {
    return JSON.parse(stored);
  }
  return { type: "FeatureCollection", features: [] };
}

function Toolbar() {
  const { editor, annotations, undo, redo, canUndo, canRedo } =
    useAnnotationsContext();

  // Save annotations when they change
  React.useEffect(() => {
    localStorage.setItem("annotations", JSON.stringify(annotations));
  }, [annotations]);

  const handleArrow = React.useCallback(() => {
    editor.enableArrowDrawing({ strokeColor: "#3498db" });
  }, [editor]);

  const handleText = React.useCallback(() => {
    editor.enableTextDrawing({ fontSize: 16 });
  }, [editor]);

  return (
    <div className="toolbar">
      <button onClick={handleArrow}>Add Arrow</button>
      <button onClick={handleText}>Add Text</button>
      <button onClick={undo} disabled={!canUndo}>
        Undo
      </button>
      <button onClick={redo} disabled={!canRedo}>
        Redo
      </button>
      <span>Annotations: {annotations.features.length}</span>
    </div>
  );
}

function App() {
  const [annotations, setAnnotations] =
    React.useState<AnnotationCollection | null>(null);

  React.useEffect(() => {
    loadAnnotations().then(setAnnotations);
  }, []);

  if (!annotations) {
    return <div>Loading...</div>;
  }

  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <Ogma graph={graph}>
        <AnnotationsContextProvider annotations={annotations}>
          <Toolbar />
        </AnnotationsContextProvider>
      </Ogma>
    </div>
  );
}

Advanced: Multiple Ogma Instances ​

If you have multiple Ogma instances, each needs its own provider:

tsx
function Dashboard() {
  return (
    <div className="dashboard">
      <div className="left-panel">
        <Ogma graph={graph1}>
          <AnnotationsContextProvider>
            <Controls />
          </AnnotationsContextProvider>
        </Ogma>
      </div>
      <div className="right-panel">
        <Ogma graph={graph2}>
          <AnnotationsContextProvider>
            <Controls />
          </AnnotationsContextProvider>
        </Ogma>
      </div>
    </div>
  );
}

Each provider maintains independent state and editor instances.

Best Practices ​

1. Keep Provider High in the Tree ​

Place the provider high enough that all components needing annotations can access it:

tsx
// ✅ Good: Provider wraps all annotation-related components
<AnnotationsContextProvider>
  <Toolbar />
  <StylePanel />
  <AnnotationsList />
</AnnotationsContextProvider>

// ❌ Avoid: Multiple providers for the same graph
<div>
  <AnnotationsContextProvider>
    <Toolbar />
  </AnnotationsContextProvider>
  <AnnotationsContextProvider>
    <StylePanel />
  </AnnotationsContextProvider>
</div>

2. Load Initial Data Asynchronously ​

Load annotations before rendering the provider to avoid empty states:

tsx
// ✅ Good: Wait for data
function App() {
  const [annotations, setAnnotations] = useState(null);

  useEffect(() => {
    fetchAnnotations().then(setAnnotations);
  }, []);

  if (!annotations) return <Loading />;

  return (
    <AnnotationsContextProvider annotations={annotations}>
      {/* ... */}
    </AnnotationsContextProvider>
  );
}

3. Don't Mutate Context Values ​

Always create new objects when updating styles:

tsx
// ✅ Good: Create new object
setArrowStyle({
  ...arrowStyle,
  strokeColor: "#e74c3c"
});

// ❌ Avoid: Mutate existing object
arrowStyle.strokeColor = "#e74c3c";
setArrowStyle(arrowStyle);

Next Steps ​