MeldUI

Use Case: Load Saved Annotations

Seed annotations on mount from your backend so users see prior review state immediately.

Most review workflows store annotations on your backend, then re-hydrate them when the document is opened. loadAnnotations() is built for this — it waits internally for the engine’s loaded event so it’s safe to call before document-loaded fires.

Pattern

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { DocumentViewer, type Annotation, type DocumentViewerInstance } from '@meldui/vue'

const viewer = ref<DocumentViewerInstance | null>(null)

onMounted(async () => {
  const saved: Annotation[] = await fetch(`/api/documents/${docId}/annotations`).then((r) =>
    r.json(),
  )
  await viewer.value?.loadAnnotations(saved)
})
</script>

<template>
  <DocumentViewer
    ref="viewer"
    source="/doc.pdf"
    wasm-url="/pdfium.wasm"
    :features="{ annotations: true, commentThreads: true }"
  />
</template>

Loading threads too

If you’re using features.commentThreads, threads are a separate data model. Load annotations first, then threads (threads reference annotation ids):

const [annotations, threads] = await Promise.all([
  fetch(`/api/documents/${docId}/annotations`).then((r) => r.json()),
  fetch(`/api/documents/${docId}/threads`).then((r) => r.json()),
])

await viewer.value?.loadAnnotations(annotations)
viewer.value?.loadThreads(threads)

Avoiding race conditions

If your route switches between documents quickly, key the viewer on the document id so Vue unmounts and remounts cleanly:

<DocumentViewer
  :key="currentDoc.id"
  ref="viewer"
  :source="currentDoc.url"
  wasm-url="/pdfium.wasm"
  :features="{ annotations: true }"
/>

Without :key, an in-flight loadAnnotations for the previous document could land on the new one.

Server-side shape

The Annotation type is JSON-serializable and round-trips cleanly. A minimum highlight is:

{
  "id": "0190f4c0-...",
  "type": "highlight",
  "pageIndex": 0,
  "rect": { "origin": { "x": 100, "y": 600 }, "size": { "width": 300, "height": 20 } },
  "segmentRects": [{ "origin": { "x": 100, "y": 600 }, "size": { "width": 300, "height": 20 } }],
  "color": "#FFCD45",
  "opacity": 0.4,
  "selectedText": "Important sentence",
  "author": "[email protected]",
  "createdAt": "2026-05-20T14:32:00.000Z"
}

Store this exact shape in your DB and you can re-hydrate with no transformation.

See also