React Integration Guide
This section covers how to properly integrate mark-img in React projects, including instance management, state synchronization, and common patterns.
Basic Integration
tsx
import { useEffect, useRef } from 'react'
import ImageMark from 'mark-img'
import 'mark-img/dist/style.css'
function ImageAnnotator({ src }) {
const containerRef = useRef(null)
const imgMarkRef = useRef(null)
useEffect(() => {
imgMarkRef.current = new ImageMark({
el: containerRef.current,
src,
pluginOptions: {
shape: {
shapeList: [],
},
},
})
return () => {
imgMarkRef.current?.destroy()
}
}, [src])
return <div ref={containerRef} style={{ width: '100%', height: '500px' }} />
}Note
The ImageMark instance should be stored in useRef, not useState, to avoid unnecessary re-renders.
Reactive State Synchronization
Sync mark-img state to React:
tsx
import { useEffect, useRef, useState } from 'react'
import ImageMark from 'mark-img'
function ImageAnnotator({ src }) {
const containerRef = useRef(null)
const imgMarkRef = useRef(null)
const [scale, setScale] = useState(1)
const [historyInfo, setHistoryInfo] = useState({ undo: 0, redo: 0 })
const [isDrawing, setIsDrawing] = useState(false)
useEffect(() => {
imgMarkRef.current = new ImageMark({
el: containerRef.current,
src,
pluginOptions: {
shape: { shapeList: [] },
},
})
.on('scale', (s) => setScale(s))
.on('history_change', (info) => setHistoryInfo(info))
.on('shape_start_drawing', () => setIsDrawing(true))
.on('shape_end_drawing', () => setIsDrawing(false))
return () => {
imgMarkRef.current?.destroy()
}
}, [src])
return (
<div>
<div>Scale: {(scale * 100).toFixed(0)}%</div>
<div>Undoable: {historyInfo.undo} steps</div>
{isDrawing && <div>Drawing...</div>}
<div ref={containerRef} style={{ width: '100%', height: '500px' }} />
</div>
)
}Drawing Toolbar
tsx
import { ImageMarkRect, ImageMarkCircle, ImageMarkPolygon } from 'mark-img'
function Toolbar({ imgMarkRef }) {
const drawShape = (ShapeClass, data) => {
const imgMark = imgMarkRef.current
if (!imgMark) return
imgMark.getShapePlugin()?.startDrawing(new ShapeClass(data, imgMark))
}
return (
<div>
<button onClick={() => drawShape(ImageMarkRect, {
shapeName: 'rect', x: 0, y: 0, width: 0, height: 0,
})}>
Rect
</button>
<button onClick={() => drawShape(ImageMarkCircle, {
shapeName: 'circle', x: 0, y: 0, r: 0,
})}>
Circle
</button>
<button onClick={() => drawShape(ImageMarkPolygon, {
shapeName: 'polygon', points: [],
})}>
Polygon
</button>
</div>
)
}Undo/Redo Buttons
tsx
function HistoryButtons({ imgMarkRef, historyInfo }) {
return (
<div>
<button
disabled={historyInfo.undo === 0}
onClick={() => imgMarkRef.current?.getHistoryPlugin()?.undo()}
>
Undo ({historyInfo.undo})
</button>
<button
disabled={historyInfo.redo === 0}
onClick={() => imgMarkRef.current?.getHistoryPlugin()?.redo()}
>
Redo ({historyInfo.redo})
</button>
</div>
)
}Readonly & Selection Mode
tsx
function SettingsPanel({ imgMarkRef }) {
const [readonly, setReadonly] = useState(false)
const [selectMode, setSelectMode] = useState('single')
useEffect(() => {
imgMarkRef.current?.setReadonly(readonly)
}, [readonly])
useEffect(() => {
imgMarkRef.current?.getSelectionPlugin()?.mode(selectMode)
}, [selectMode])
return (
<div>
<label>
<input
type="checkbox"
checked={readonly}
onChange={(e) => setReadonly(e.target.checked)}
/>
Readonly Mode
</label>
<select value={selectMode} onChange={(e) => setSelectMode(e.target.value)}>
<option value="single">Single</option>
<option value="multiple">Multiple</option>
</select>
</div>
)
}Context Menu
tsx
function ImageAnnotator() {
const imgMarkRef = useRef(null)
const tmpShapeRef = useRef(null)
const [contextMenu, setContextMenu] = useState(null)
useEffect(() => {
imgMarkRef.current = new ImageMark({ /* ... */ })
.on('shape_context_menu', (evt, shapeInstance) => {
evt.preventDefault()
tmpShapeRef.current = shapeInstance
setContextMenu({ x: evt.clientX, y: evt.clientY, type: 'shape' })
})
.on('container_context_menu', (evt) => {
evt.preventDefault()
setContextMenu({ x: evt.clientX, y: evt.clientY, type: 'container' })
})
const hide = () => setContextMenu(null)
document.addEventListener('click', hide)
return () => {
imgMarkRef.current?.destroy()
document.removeEventListener('click', hide)
}
}, [])
const handleDelete = () => {
if (tmpShapeRef.current) {
imgMarkRef.current?.getShapePlugin()?.removeNode(tmpShapeRef.current)
}
setContextMenu(null)
}
return (
<div>
<div ref={containerRef} style={{ width: '100%', height: '500px' }} />
{contextMenu && (
<div style={{
position: 'fixed',
left: contextMenu.x,
top: contextMenu.y,
}}>
{contextMenu.type === 'shape' && (
<button onClick={handleDelete}>Delete</button>
)}
{contextMenu.type === 'container' && (
<button onClick={() => {
imgMarkRef.current?.getShapePlugin()?.removeAllNodes()
setContextMenu(null)
}}>
Delete All
</button>
)}
</div>
)}
</div>
)
}Data Persistence
Combine with localStorage for auto-save and restore:
tsx
import { debounce } from 'lodash-es'
function ImageAnnotator({ src }) {
const containerRef = useRef(null)
const imgMarkRef = useRef(null)
const initData = JSON.parse(localStorage.getItem('shapeList') || '[]')
useEffect(() => {
const autoSave = debounce(() => {
const data = imgMarkRef.current?.getShapePlugin()?.data || []
localStorage.setItem('shapeList', JSON.stringify(data))
}, 300)
imgMarkRef.current = new ImageMark({
el: containerRef.current,
src,
pluginOptions: {
shape: { shapeList: initData },
},
}).on('shape_plugin_data_change', autoSave)
return () => {
imgMarkRef.current?.destroy()
}
}, [src])
return <div ref={containerRef} style={{ width: '100%', height: '500px' }} />
}Switching Image Source
When src changes, destroy the old instance and create a new one:
tsx
useEffect(() => {
imgMarkRef.current = new ImageMark({
el: containerRef.current,
src,
pluginOptions: {
shape: { shapeList: [] },
},
})
return () => {
imgMarkRef.current?.destroy()
}
}, [src]) // src as dependencyFor a complete React integration example, see src/views/BeautifulPresentation.tsx in the project source.