Skip to content

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 ​

sh
pnpm add @sealpixel/react
sh
npm install @sealpixel/react

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

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

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

ts
/**
 * 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 ​

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