React
@sealpixel/react wraps <sealpixel-editor> with @lit/react: a component whose props are the element's properties and whose on… props are its events. React 18 and 19.
Install
pnpm add @sealpixel/reactnpm install @sealpixel/reactreact 18 or 19 is a peer dependency. The wrapper does not import react-dom. @sealpixel/element and @sealpixel/core come with the package.
The starter
examples/starters/react, a Vite app. Open it on StackBlitz. The component, quoted from the starter:
import type { ExportResult } from '@sealpixel/core';
import { SealpixelEditor } from '@sealpixel/react';
import { useState } from 'react';
const initial = {
version: 1,
source: { id: 'landscape', width: 120, height: 80 },
plugins: { finetune: 1, filter: 1 },
ops: [
{ type: 'finetune.set', id: 'shared-1', params: { contrast: 0.2, saturation: 0.3 } },
{ type: 'filter.set', id: 'shared-2', preset: 'warm', strength: 0.6 },
],
};
export function App() {
const [editLog, setEditLog] = useState<unknown>(initial);
const [processed, setProcessed] = useState(
'Press Download: the file name carries the SHA-256 of the result.',
);
return (
<>
<SealpixelEditor
src="/landscape.png"
theme="dark"
editLog={editLog}
onChange={(e) => setEditLog(e.detail)}
onProcess={(e) => {
const r = (e as CustomEvent<ExportResult>).detail;
setProcessed(`exported ${r.width}x${r.height} sha256 ${r.hash}`);
}}
/>
<output data-testid="processed">{processed}</output>
</>
);
}editLog plus onChange is controlled use: the editor renders the log you hold, and every change hands you { log, index } to hold next. Store that object as it is — it is what editLog takes back. For uncontrolled use, pass defaultEditLog once instead and read the log from onChange when you want to save it.
onProcess fires when the user presses Download; the detail is an ExportResult with the bytes, the dimensions and the SHA-256 of the render.
Build setup
Vite needs nothing beyond the React plugin:
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({ plugins: [react()] });Any bundler that handles new Worker(new URL(…, import.meta.url), { type: 'module' }) and emits .wasm as an asset works; webpack 5 does both out of the box.
Server-side rendering
The component runs in the browser only: it registers a custom element and starts a Web Worker. In Next.js, put it in a client component ('use client' at the top of the file) and, where the page itself renders on the server, load that component with next/dynamic and ssr: false. Remix and React Router apps use the same pattern with React.lazy inside a client-only boundary. Rendering the tag on the server is harmless — it is just an unknown element until the script runs — but the props that are objects, like editLog, only reach the element on the client.
Headless
useSealpixelSession gives one session per component instance, its state through useSyncExternalStore, disposed on unmount. Under StrictMode React runs the cleanup once in development; the hook then replaces the disposed session, so list session in the dependencies of effects that use it. Everything the UI does goes through that session; see Getting started for the calls.
/**
* Headless use: one session per component instance, state via
* `useSyncExternalStore`. Dispose happens on unmount.
*/
export function useSealpixelSession(options: SessionOptions): {
session: EditorSession;
state: SessionState;
} {
const [session, setSession] = React.useState(() => createEditorSession(options));
const disposed = React.useRef<EditorSession | null>(null);
const latestOptions = React.useRef(options);
latestOptions.current = options;
React.useEffect(() => {
// StrictMode, and a component that React hides and shows again, run the
// cleanup and then this effect again. The disposed session cannot be used
// again, so replace it.
if (disposed.current === session) {
setSession(createEditorSession(latestOptions.current));
return;
}
return () => {
disposed.current = session;
session.dispose();
};
}, [session]);
const state = React.useSyncExternalStore(
(listener) => session.subscribe(() => listener()),
() => session.getState(),
() => session.getState(),
);
return { session, state };
}Server rendering
Above the client limit the browser cannot export, see Working copy. Render the original on your server with @sealpixel/render: it takes the original and the edit log from getEditLog(). See Render on your server and the Node starter. The renderer does not depend on React. In Next.js it runs in a route handler.
TypeScript
import type { SealpixelEditorProps, SessionState } from '@sealpixel/react';
import type { ExportResult, SealpixelError, SourceDescriptor } from '@sealpixel/core';The event props are typed: onChange gets CustomEvent<{ log: unknown; index: number }>, onProcess CustomEvent<ExportResult>, onError CustomEvent<SealpixelError>, onLoad CustomEvent<SourceDescriptor>, onReady CustomEvent<void>. onReady also runs for the session that the element creates when React inserts it.