React
@infinite-canvas-tutorial/react provides React 18/19 bindings for the existing Spectrum canvas, including typed events, API access, and a shared App lifecycle.
npm install @infinite-canvas-tutorial/react'use client';
import { useRef } from 'react';
import {
InfiniteCanvas,
type InfiniteCanvasHandle,
} from '@infinite-canvas-tutorial/react/spectrum';
export function Editor() {
const canvas = useRef<InfiniteCanvasHandle>(null);
return (
<>
<button onClick={() => canvas.current?.api?.undo()}>Undo</button>
<InfiniteCanvas
ref={canvas}
style={{ width: '100%', height: 600 }}
initialNodes={[
{ id: 'rect', type: 'rect', x: 50, y: 50, width: 100, height: 80, fill: '#ff8400' },
]}
onNodesChange={(nodes) => console.log(nodes)}
fallback={<span>Loading canvas…</span>}
/>
</>
);
}Give the container an explicit height. Browser dependencies and Custom Element registration are deferred until client mount, including in Next.js Client Components. The root entry and /spectrum currently export the same component.
Initialization and API
initialNodesis copied once per canvas creation and inserted into ECS after asynconReadycompletes. Later prop changes do not overwrite user edits; useapi.updateNodes()for updates.initialAppStateseeds canvas state; subsequent edits useapi.setAppState().- The ref exposes live
apiandelementgetters, initiallynull. The element is the actual Web Component, suitable for additional DOM events. onAPIChange(api)publishes the API and receivesnullon removal.themeupdates Spectrum and the canvas. Lit localization is global, so canvases in the same page share thelocale.- Changing
renderer,shaderCompilerPath, orinitializationTimeoutrecreates the canvas.
Events and loading
Typed callbacks include onChange, onNodesChange, onAppStateChange, onSelectedNodesChange, onCameraZoomChange, and onResize. Updating callbacks does not recreate the canvas.
Use fallback for loading content and renderError for custom errors. onError reports runtime loading, readiness timeout, locale, and async preparation failures. initializationTimeout defaults to 30000 milliseconds; 0 disables the runtime/GPU readiness timeout.
Plugins and multiple canvases
Create a shared runtime outside component render:
import { InfiniteCanvas, createCanvasRuntime } from '@infinite-canvas-tutorial/react';
import { LassoPlugin } from '@infinite-canvas-tutorial/lasso';
const runtime = createCanvasRuntime({
plugins: [LassoPlugin],
loadUI: () => import('@infinite-canvas-tutorial/lasso/spectrum'),
});
export function Compare() {
return (
<>
<InfiniteCanvas runtime={runtime} style={{ height: 400 }} />
<InfiniteCanvas runtime={runtime} style={{ height: 400 }} />
</>
);
}DefaultPlugins and UIPlugin are included. Plugin configuration is fixed at startup; release all canvases before replacing the runtime. Concurrent distinct runtimes are rejected because the Web Components use global initialization queues.
Each canvas owns its API and subscriptions. The App exits after its last canvas is removed. StrictMode's immediate setup/cleanup/setup shares pending startup. React children are mounted in the Web Component's light DOM and support its named slots.
Async preparation
onReady(api, { signal }) may return a Promise. Pass the signal to cancellable tasks and check it after awaits before using the API:
<InfiniteCanvas
initialNodes={nodes}
onReady={async (api, { signal }) => {
const response = await fetch('/assets/config.json', { signal });
const config = await response.json();
if (signal.aborted) return;
api.setAppState(config);
}}
/>Uploading, persistence, and collaboration belong to the host application. Providers and selector hooks are planned as subsequent additions.