Skip to content

Event System

Annota provides a comprehensive event system to respond to annotation lifecycle changes.

Annota emits four core events:

EventTriggered WhenData
createAnnotationNew annotation createdAnnotation
updateAnnotationAnnotation modifiedAnnotation
deleteAnnotationAnnotation deletedAnnotation
selectionChangedSelection changes{ selected: string[] }

Access the annotator instance and register event handlers:

import { useAnnotator } from "annota";
import { useEffect } from "react";
function AnnotationLogger() {
const annotator = useAnnotator();
useEffect(() => {
if (!annotator) return;
const handleCreate = (annotation) => {
console.log("Annotation created:", annotation);
};
const handleUpdate = (annotation) => {
console.log("Annotation updated:", annotation);
};
const handleDelete = (annotation) => {
console.log("Annotation deleted:", annotation);
};
const handleSelection = ({ selected }) => {
console.log("Selection changed:", selected);
};
// Register handlers
annotator.on("createAnnotation", handleCreate);
annotator.on("updateAnnotation", handleUpdate);
annotator.on("deleteAnnotation", handleDelete);
annotator.on("selectionChanged", handleSelection);
// Cleanup
return () => {
annotator.off("createAnnotation", handleCreate);
annotator.off("updateAnnotation", handleUpdate);
annotator.off("deleteAnnotation", handleDelete);
annotator.off("selectionChanged", handleSelection);
};
}, [annotator]);
return null;
}

Fired when a new annotation is created.

function AutoSave() {
const annotator = useAnnotator();
useEffect(() => {
if (!annotator) return;
const handleCreate = async (annotation) => {
await fetch("/api/annotations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(annotation),
});
console.log("Saved annotation:", annotation.id);
};
annotator.on("createAnnotation", handleCreate);
return () => annotator.off("createAnnotation", handleCreate);
}, [annotator]);
return null;
}
function AnnotationCounter() {
const [counts, setCounts] = useState({ point: 0, rectangle: 0, polygon: 0 });
const annotator = useAnnotator();
useEffect(() => {
if (!annotator) return;
const handleCreate = (annotation) => {
setCounts((prev) => ({
...prev,
[annotation.shape.type]: prev[annotation.shape.type] + 1,
}));
};
annotator.on("createAnnotation", handleCreate);
return () => annotator.off("createAnnotation", handleCreate);
}, [annotator]);
return (
<div>
<div>Points: {counts.point}</div>
<div>Rectangles: {counts.rectangle}</div>
<div>Polygons: {counts.polygon}</div>
</div>
);
}

Fired when an annotation is modified (properties, style, or shape).

function ServerSync() {
const annotator = useAnnotator();
useEffect(() => {
if (!annotator) return;
const handleUpdate = async (annotation) => {
await fetch(`/api/annotations/${annotation.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(annotation),
});
};
annotator.on("updateAnnotation", handleUpdate);
return () => annotator.off("updateAnnotation", handleUpdate);
}, [annotator]);
return null;
}

Fired when an annotation is deleted.

function DeletionLogger() {
const annotator = useAnnotator();
useEffect(() => {
if (!annotator) return;
const handleDelete = (annotation) => {
console.log(`Deleted annotation ${annotation.id} (${annotation.shape.type})`);
// Sync deletion to server
fetch(`/api/annotations/${annotation.id}`, { method: "DELETE" });
};
annotator.on("deleteAnnotation", handleDelete);
return () => annotator.off("deleteAnnotation", handleDelete);
}, [annotator]);
return null;
}

Fired when the selection changes.

function BulkOperations() {
const [selectedIds, setSelectedIds] = useState([]);
const annotator = useAnnotator();
useEffect(() => {
if (!annotator) return;
const handleSelection = ({ selected }) => {
setSelectedIds(selected);
};
annotator.on("selectionChanged", handleSelection);
return () => annotator.off("selectionChanged", handleSelection);
}, [annotator]);
const deleteSelected = () => {
selectedIds.forEach((id) => annotator?.deleteAnnotation(id));
};
const changeColor = (color: string) => {
selectedIds.forEach((id) => {
annotator?.updateAnnotation(id, { style: { fill: color } });
});
};
return (
<div>
<button onClick={deleteSelected} disabled={selectedIds.length === 0}>
Delete Selected ({selectedIds.length})
</button>
<button onClick={() => changeColor("#FF0000")}>Red</button>
<button onClick={() => changeColor("#00FF00")}>Green</button>
</div>
);
}

Avoid excessive event firing:

import { debounce } from "lodash";
function DebouncedSync() {
const annotator = useAnnotator();
const debouncedSyncRef = useRef(
debounce(async (annotation) => {
await fetch(`/api/annotations/${annotation.id}`, {
method: "PATCH",
body: JSON.stringify(annotation),
});
}, 500)
);
useEffect(() => {
if (!annotator) return;
const handleUpdate = (annotation) => {
debouncedSyncRef.current(annotation);
};
annotator.on("updateAnnotation", handleUpdate);
return () => annotator.off("updateAnnotation", handleUpdate);
}, [annotator]);
return null;
}

Filter events based on conditions:

function TumorAnnotationLogger() {
const annotator = useAnnotator();
useEffect(() => {
if (!annotator) return;
const handleCreate = (annotation) => {
// Only log tumor annotations
if (annotation.properties?.type === "tumor") {
console.log("Tumor annotation created:", annotation.id);
}
};
annotator.on("createAnnotation", handleCreate);
return () => annotator.off("createAnnotation", handleCreate);
}, [annotator]);
return null;
}

Always Clean Up

Remove event listeners in cleanup functions to prevent memory leaks.

Handle Async Properly

Use async/await for server operations with proper error handling.

Use Stable References

Use useCallback or useRef for complex handlers.

Error Handling

Wrap handlers in try/catch to prevent errors from breaking the app.