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:

Importstsx
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:

CSS Importstsx
// 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).

EditorPage.tsx — Full Exampletsx
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>
  );
}
⚠️
StrictMode double-mount
React 18+ StrictMode intentionally mounts and unmounts components twice in development. The initRef guard ensures the engine is only created once. Without it, you would see duplicate canvases or initialization errors.
💡
Canvas element
Never render <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.

Export Pluginstsx
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.

useEditorContexttsx
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.

useEditorStoretsx
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.

useHistorytsx
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.

useTooltsx
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.

useTranslationtsx
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.

Engine API Examplestsx
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:

Theme CSStsx
// 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:

Theme Proptsx
// Set theme on EditorShell
<EditorShell theme="light" />
<EditorShell theme="dark" />
💡
Dynamic theming
You can switch themes at runtime by dynamically importing CSS files and updating the theme prop. The editor UI will re-render with the new theme instantly.

Next Steps