React Integration
A comprehensive guide for integrating Design on Web into a React application. This covers engine setup, context providers, hooks, event handling, and theming.
Setup
Import the core engine and UI components from their respective packages:
import { EditorEngine } from '@design-on-web/core';
import { EditorContext, EditorShell } from '@design-on-web/ui';You also need to import the CSS theme files for the UI to render:
// Required CSS — order matters
import '@design-on-web/ui/themes/variables.css';
import '@design-on-web/ui/themes/light.css'; // or dark.css
import '@design-on-web/ui/components/EditorShell.css';Creating the Engine
The canvas element must be created programmatically using document.createElement — it should not be placed in the React render tree. Fabric.js manages the canvas DOM directly, and mixing it with React's virtual DOM causes rendering conflicts.
Use useEffect combined with a useRef guard to prevent double initialization in React StrictMode (which mounts components twice in development).
import { useRef, useEffect, useState } from 'react';
import { EditorEngine } from '@design-on-web/core';
import { EditorContext, EditorShell } from '@design-on-web/ui';
import { PsdExportPlugin } from '@design-on-web/export-psd';
import { PdfExportPlugin } from '@design-on-web/export-pdf';
import { SvgExportPlugin } from '@design-on-web/export-svg';
import { ImageExportPlugin } from '@design-on-web/export-image';
// CSS
import '@design-on-web/ui/themes/variables.css';
import '@design-on-web/ui/themes/light.css';
import '@design-on-web/ui/components/EditorShell.css';
export default function EditorPage() {
const [engine, setEngine] = useState<EditorEngine | null>(null);
const initRef = useRef(false);
useEffect(() => {
// Prevent double initialization in React StrictMode
if (initRef.current) return;
initRef.current = true;
async function init() {
// Create canvas element programmatically — NOT in the React render tree.
// Fabric.js manages the canvas DOM directly; placing it inside JSX
// causes conflicts with React's virtual DOM.
const canvasEl = document.createElement('canvas');
const eng = new EditorEngine({
canvasElement: canvasEl,
width: 1080,
height: 1080,
backgroundColor: '#ffffff',
});
// Register export plugins (optional)
await eng.registerPlugin(PsdExportPlugin);
await eng.registerPlugin(PdfExportPlugin);
await eng.registerPlugin(SvgExportPlugin);
await eng.registerPlugin(ImageExportPlugin);
setEngine(eng);
}
init();
return () => {
// Cleanup on unmount
if (initRef.current) {
// engine?.destroy() is called via state cleanup
}
};
}, []);
// Destroy engine when component unmounts
useEffect(() => {
return () => {
engine?.destroy();
};
}, [engine]);
if (!engine) return <div>Loading editor...</div>;
return (
<div style={{ width: '100vw', height: '100vh' }}>
<EditorContext.Provider value={engine}>
<EditorShell theme="light" />
</EditorContext.Provider>
</div>
);
}initRef guard ensures the engine is only created once. Without it, you would see duplicate canvases or initialization errors.<canvas> in JSX and pass it to EditorEngine. Always use document.createElement('canvas'). The EditorShell component handles mounting the canvas into the DOM automatically.Registering Export Plugins
Export plugins add support for additional file formats beyond the built-in PNG, JPEG, and WebP. Each plugin is a separate package to keep the core bundle small.
import { PsdExportPlugin } from '@design-on-web/export-psd';
import { PdfExportPlugin } from '@design-on-web/export-pdf';
import { SvgExportPlugin } from '@design-on-web/export-svg';
import { ImageExportPlugin } from '@design-on-web/export-image';
// After engine creation:
await eng.registerPlugin(PsdExportPlugin);
await eng.registerPlugin(PdfExportPlugin);
await eng.registerPlugin(SvgExportPlugin);
await eng.registerPlugin(ImageExportPlugin);Register plugins after creating the engine but before the user interacts with the export dialog. All registerPlugin calls return promises that resolve when the plugin is ready.
Using Hooks
The @design-on-web/ui package provides several React hooks for interacting with the editor from custom components. All hooks must be called within an EditorContext.Provider.
useEditorContext()
Returns the EditorEngine instance from context. Use this to call engine methods directly.
import { useEditorContext } from '@design-on-web/ui';
function MyToolbar() {
const engine = useEditorContext();
const handleAddRect = () => {
engine.addShape('rect', { left: 100, top: 100, fill: '#4f46e5' });
};
return <button onClick={handleAddRect}>Add Rectangle</button>;
}useEditorStore(selector)
Subscribes to the Zustand store and returns the selected state slice. The component re-renders only when the selected value changes.
import { useEditorStore } from '@design-on-web/ui';
function ZoomIndicator() {
const zoom = useEditorStore((s) => s.zoom);
const selectedIds = useEditorStore((s) => s.selectedObjectIds);
return (
<div>
<span>Zoom: {Math.round(zoom * 100)}%</span>
<span>Selected: {selectedIds.length} objects</span>
</div>
);
}useHistory()
Returns undo/redo controls and their availability state.
import { useHistory } from '@design-on-web/ui';
function UndoRedoButtons() {
const { undo, redo, canUndo, canRedo } = useHistory();
return (
<div>
<button onClick={undo} disabled={!canUndo}>Undo</button>
<button onClick={redo} disabled={!canRedo}>Redo</button>
</div>
);
}useTool()
Returns the active tool name and a setter to switch tools.
import { useTool } from '@design-on-web/ui';
function ToolSelector() {
const { activeTool, setTool } = useTool();
return (
<div>
<button
onClick={() => setTool('select')}
className={activeTool === 'select' ? 'active' : ''}
>
Select
</button>
<button
onClick={() => setTool('rect')}
className={activeTool === 'rect' ? 'active' : ''}
>
Rectangle
</button>
</div>
);
}useTranslation()
Returns the i18n translation function for localizing UI labels.
import { useTranslation } from '@design-on-web/ui';
function LocalizedLabel() {
const { t } = useTranslation();
return <span>{t('toolbar.undo')}</span>;
}Accessing the Engine API
Once you have the engine instance (via useEditorContext), you have full access to the EditorEngine API for adding content, exporting, serializing, and listening to events.
const engine = useEditorContext();
// Add shapes
engine.addShape('rect', { left: 100, top: 100, width: 200, height: 150, fill: '#ff0000' });
engine.addShape('circle', { left: 300, top: 200, radius: 75, fill: '#00ff00' });
// Add text
engine.addText('Hello World', { left: 150, top: 400, fontSize: 48, fill: '#333' });
// Add image
await engine.addImage('https://example.com/photo.jpg', { left: 50, top: 50 });
// Export to PNG at 2x resolution
const blob = await engine.exportAs('png', { multiplier: 2 });
// Serialize / deserialize
const json = engine.toJSON();
await engine.loadJSON(json);
// Subscribe to events
engine.getEventBus().on('object:selected', (e) => {
console.log('Selected objects:', e.objectIds);
});
engine.getEventBus().on('object:modified', (e) => {
console.log('Object changed:', e);
});See the EditorEngine API Reference for the complete list of methods.
Theming
Design on Web ships with light and dark themes. Choose which theme CSS file to import:
// Light theme
import '@design-on-web/ui/themes/variables.css';
import '@design-on-web/ui/themes/light.css';
import '@design-on-web/ui/components/EditorShell.css';
// --- OR ---
// Dark theme
import '@design-on-web/ui/themes/variables.css';
import '@design-on-web/ui/themes/dark.css';
import '@design-on-web/ui/components/EditorShell.css';Then set the theme prop on EditorShell to match:
// Set theme on EditorShell
<EditorShell theme="light" />
<EditorShell theme="dark" />theme prop. The editor UI will re-render with the new theme instantly.Next Steps
- EditorEngine API — full method reference
- Store API — subscribe to editor state
- Events API — listen to editor events
- Export API — export formats and options