React — islands

@atolljs/react-island — a worker-hosted React (or imperative proxy-DOM) tree mounted as an ordinary element in a React shell. The worker's render loop produces serialized DOM ops; the main thread just replays them.

Live demo — React in the worker

Four islands, React rendered inside the workers: a counter, a second counter instance, a notes composer, and a 1,000,000-record incident benchmark — the two counters share ONE client (both mounts live in a single worker, counter@N keys, one OS thread), notes gets its own client on the same script, and the incidents benchmark carries a third worker through its lazy contract module. The shell is just a thin <Island/> host — worker emits land in onEvent → React state → the status line.

The incident benchmark is the real-world case for offloading a heavy component. One million incidents exist as lazily generated logical rows — the component is a fixed-height virtualized scroller that renders only the ~22 visible rows plus overscan, no matter where you scroll. Each scroll event crosses the island protocol as a structured payload (the driver stamps scrollTop), React re-computes the window worker-side, and the commit rides back as an op batch — the stats line reports the worker-side re-render time, and a rendered emit updates the shell's status line. The main thread never touches more than a handful of DOM nodes; a million rows of state, generation, and diffing all stay off it.

If the frame is blank, the examples aren't up — run npm run dev:all (dev servers) or npm run serve:all (built apps) from the repository root.

The proxy API

ExportSignatureWhat it does
lazyIslandlazyIsland(loader: () => Promise<{ default: A } | { app, worker? } | A>): FC<IslandAppProps<A> & IslandShellProps>React.lazy mirrored — the returned component suspends on the dynamic import (a real bundler split point), then mounts the resolved app with props inferred from its signature. Contract modules { app, worker } carry their own worker factory — that's how the incidents island gets a dedicated worker.
islandComponentislandComponent<P>('name') | islandComponent(StampedApp)The pure-contract proxy — the shell never imports the implementation; a registry key + a type-only props import is the whole contract.
Island<Island app={ref|name} client|worker props onEvent slots onReady/>The underlying building block — declarative mountIsland as a component. props dedups by serialized identity.
islandAppislandApp('name', app)Stamps an app (component or {imperative} def) with its registry name — a data property, so references survive minification.
defineReactPolyWorkerdefineReactPolyWorker({ apps }) — react-island/workerRegistry worker — one script serving a whole apps map; islands mount by name and several may share one client/worker. Components wrap through reactIslandApp automatically.
defineReactMonoWorkerdefineReactMonoWorker(app) — react-island/workerInstance worker — the 1:1 topology: one script, one app, mounted namelessly. Its bundle carries only that app's dependencies.
Slot / emit<Slot name> / emit(name, payload)Transclusion: worker markup hands a real element to the shell's slots map (canvases, Monaco, AG Grid). emit is the island→shell event channel.

Mounting — the proxies make islands look local

The counters mount through plain <Island/> elements on a shared connectIslandWorker client; notes mounts through islandComponent('notes') — a registry key is the whole contract; incidents mounts through lazyIsland with a contract module, so its chunk code-splits and its worker is self-contained. <Suspense> covers the module load; the proxy's fallback prop covers the worker-mount window — mounting can't suspend because a suspended tree never commits and the container must be in the DOM first.

examples/react-dom-worker/src/shell.tsxfrontend
/**
 * The React islands demo — REACT RUNNING INSIDE WORKERS.
 *
 * Each island below mounts an app from `worker/react.worker.tsx`'s
 * registry: ordinary React components (useState/useLayoutEffect) rendered
 * by react-reconciler into serialized DOM ops. The worker bundle carries
 * React; this page is just a thin host that replays its ops.
 *
 * What changes versus main.ts (the seven-island demo):
 *   - The shell itself is React — each island is an <Island/> element, a
 *     islandComponent facade, or a lazyIsland contract module.
 *   - `client` (not `worker:`) is passed so the doorbell choice travels
 *     with the connection; the two counters deliberately share ONE client
 *     so their islands live in the same OS worker.
 *   - Mediation is plain React state: worker emits land in onEvent →
 *     setState → the status line.
 *
 * The seven-island demo lives in index.html — this page is the small
 * framework-island edition matching vue/solid/svelte/angular-shell.html.
 */
import { useRef, useState, Suspense } from 'react';
import type { ReactElement, ReactNode } from 'react';
import { createRoot } from 'react-dom/client';
import { Island, islandComponent, lazyIsland } from '@atolljs/react-island';
import { connectIslandWorker } from '@atolljs/islands';
import type { IslandHandle, Mode } from '@atolljs/islands';

/** The registry worker — one script serving all three React apps. */
const reactWorker = (): Worker =>
  new Worker(new URL('./worker/react.worker.tsx', import.meta.url), { type: 'module' });

// SharedArrayBuffer only exists in cross-origin-isolated contexts — on
// hosts without COOP/COEP (GitHub Pages where coi-sw.js didn't take, or a
// browser without `credentialless`) the doorbell can't bind, so every
// island runs its 50ms poll transport instead of push.
const isolated =
  typeof SharedArrayBuffer !== 'undefined' &&
  (typeof window.crossOriginIsolated === 'undefined' || window.crossOriginIsolated);
const initialMode: Mode = isolated ? 'push' : 'poll';

/**
 * Two clients = two OS workers running the same script. The counters share
 * one client (one worker, two 'counter@N' instances); notes gets its own —
 * and the incidents benchmark carries a third worker through its contract
 * module below.
 */
const counterClient = connectIslandWorker({ worker: reactWorker, doorbell: isolated });
const notesClient = connectIslandWorker({ worker: reactWorker, doorbell: isolated });

/**
 * The facades — mount worker apps this shell never imports. Notes goes
 * through an eager proxy component (the string is the registry key);
 * incidents is lazy: a dynamic import + contract module that carries its
 * OWN worker, so the 1M-row benchmark can't contend with anything else
 * and its chunk only loads when the island mounts.
 */
const NotesIsland = islandComponent<{ title?: string }>('notes');
const IncidentsIsland = lazyIsland(() => import('./incidents.island'));

/* ── Shell ──────────────────────────────────────────────────────────────── */

const loading = (
  <div className="island-root" style={{ color: '#7d8a9c' }}>
    loading worker module…
  </div>
);

function IslandPanel(props: {
  title: string;
  badge?: string;
  children: ReactNode;
}): ReactElement {
  return (
    <section className="island">
      <div className="island-head">
        <span>{props.title}</span>
        <span className="badge">{props.badge ?? 'worker …'}</span>
      </div>
      {props.children}
    </section>
  );
}

export function Shell(): ReactElement {
  // Mediation state — worker emits land here; the status line renders it.
  const [status, setStatus] = useState('mounting islands…');
  const [mode, setModeState] = useState<Mode>(initialMode);
  const [pids, setPids] = useState<Record<string, string>>({});
  const [statsTick, setStatsTick] = useState(0);

  /** Every mounted island's handle — setMode + the aggregate stats read them. */
  const handles = useRef(new Map<string, IslandHandle>());
  const modeRef = useRef(mode);
  modeRef.current = mode;

  const ready = (key: string) => (h: IslandHandle): void => {
    handles.current.set(key, h);
    h.setMode(modeRef.current); // applies the user's pick to late-mounting islands
    setPids((p) => ({ ...p, [key]: `worker ${h.pid}` }));
    setStatsTick((t) => t + 1);
  };
  const bump = (): void => setStatsTick((t) => t + 1);

  const setMode = (next: Mode): void => {
    setModeState(next);
    for (const h of handles.current.values()) h.setMode(next);
    bump();
  };

  const flushes = [...handles.current.values()].reduce((a, i) => a + i.flushCalls, 0);
  const ops = [...handles.current.values()].reduce((a, i) => a + i.opsApplied, 0);
  void statsTick; // re-render trigger for the aggregate read

  const counterOpts = (label: string, key: string) => ({
    client: counterClient,
    app: 'counter',
    props: { label },
    mode: initialMode,
    onReady: ready(key),
    onActivity: bump,
    onEvent: (name: string, payload: unknown) => {
      const p = payload as { count?: number; label?: string };
      if (name === 'incremented')
        setStatus(`${p.label} counter → ${p.count} (React state stayed in the worker)`);
    },
  });

  return (
    <>
      <h1>React islands — React in the worker</h1>
      <p style={{ font: '12px monospace', color: '#9aa4b2', marginTop: -8 }}>
        registry worker + shared client — the worker bundle carries React, the shell is a thin{' '}
        <code>{'<Island/>'}</code> + <code>islandComponent</code>/<code>lazyIsland</code> host.{' '}
        <a href="./index.html" style={{ color: '#7fb6ff' }}>framework-free shell →</a>
      </p>

      <div id="transport-bar">
        <span>transport:</span>
        <label
          id="transport-toggle"
          title={isolated ? 'push via SharedArrayBuffer doorbell — off falls back to 50ms polling' : 'needs cross-origin isolation (no SharedArrayBuffer)'}
        >
          <input
            id="push-toggle"
            type="checkbox"
            checked={mode === 'push'}
            disabled={!isolated}
            onChange={() => setMode(mode === 'push' ? 'poll' : 'push')}
          />
          push (SAB doorbell)
        </label>
        <span id="transport-stats">
          sync: {mode} · flush calls: {flushes} · ops applied: {ops}
        </span>
      </div>
      <div id="status-line">{status}</div>

      <IslandPanel title="app: counter (react-reconciler — this worker has no DOM)" badge={pids.counter}>
        <Island {...counterOpts('alpha', 'counter')} className="island-root" />
      </IslandPanel>

      <IslandPanel
        title="app: counter — second instance (SAME worker as the first, one client)"
        badge={pids.counter2}
      >
        <Island {...counterOpts('beta', 'counter2')} className="island-root" />
      </IslandPanel>

      <IslandPanel
        title="app: notes — islandComponent facade (attrs ARE the props)"
        badge={pids.notes}
      >
        <NotesIsland
          client={notesClient}
          title="react island"
          onReady={ready('notes')}
          onActivity={bump}
          onEvent={(name, payload) => {
            const p = payload as { text?: string; total?: number };
            if (name === 'noteAdded')
              setStatus(`notes island emitted noteAdded → "${p.text}" (${p.total} total)`);
          }}
          containerProps={{ className: 'island-root' }}
        />
      </IslandPanel>

      <IslandPanel
        title="app: incidents — lazyIsland + contract module (own worker, 1M rows)"
        badge={pids.incidents}
      >
        <Suspense fallback={loading}>
          <IncidentsIsland
            // The contract supplies the worker; workerOptions carries the
            // doorbell choice through to the shorthand client it builds.
            workerOptions={{ doorbell: isolated }}
            mode={initialMode}
            onReady={ready('incidents')}
            onActivity={bump}
            onEvent={(name, payload) => {
              const p = payload as { start?: number; end?: number; ms?: number };
              if (name === 'rendered')
                setStatus(
                  `incidents island rendered rows ${p.start?.toLocaleString()}–${p.end?.toLocaleString()} in ${p.ms?.toFixed(1)}ms (of 1,000,000)`,
                );
            }}
            containerProps={{ className: 'island-root' }}
          />
        </Suspense>
      </IslandPanel>
    </>
  );
}

// Guarded so the module is import-safe in tests (they render <Shell/> directly).
const rootEl = document.getElementById('root');
if (rootEl) createRoot(rootEl).render(<Shell />);

The worker side — React running in the worker

The worker bundle carries React itself — defineReactPolyWorker serves the whole apps map from one script, and a real react-reconciler drives the proxy DOM with state staying worker-side. Components are unremarkable React: hooks, controlled inputs, emit(name, payload) for the island → shell channel. The only rules: no DOM access, serializable props, and handlers receive the plain EventPayload wire object instead of a SyntheticEvent — handler() in the file adapts it to JSX's event prop types. See the Islands page for modes, slots, and lifecycle.

examples/react-dom-worker/src/worker/react.worker.tsxfrontend
/**
 * React island worker — a registry worker serving React apps through
 * `reactIslandApp` (react-reconciler bound to the instance's op stream).
 * Its bundle carries React but no Vue/Solid/Svelte/Angular — the point of
 * the demo: islands are framework-agnostic over one op protocol, and this
 * worker's apps are the same counter/notes/incidents set the other
 * framework shells mount.
 *
 * Unlike the other frameworks' async schedulers, React's commit runs inside
 * the dispatch's sync lane — `useLayoutEffect` fires while the task still
 * holds the instance, so `emit` from a commit-phase effect needs no
 * `runInInstance` re-entry (see TableApp's `rowsChanged` in apps.tsx).
 */
import { useLayoutEffect, useRef, useState } from 'react';
import { defineReactPolyWorker, emit } from '@atolljs/react-island/worker';
import { islandApp, type EventPayload } from '@atolljs/islands/worker';

/**
 * Worker-side event handlers receive the plain wire payload — { type, value,
 * checked, key, scrollTop } — not a SyntheticEvent. `handler()` adapts them
 * to the DOM event prop types JSX expects; the cast is the whole story.
 */
const handler = <E,>(fn: (e: EventPayload) => void): ((e: E) => void) =>
  fn as unknown as (e: E) => void;

/**
 * 'counter' — the canonical framework island: a `label` wire prop, a
 * `useState` count, and an 'incremented' emit for the shell's status line.
 */
const CounterApp = islandApp('counter', function CounterApp({
  label = 'count',
}: {
  label?: string;
}) {
  const [count, setCount] = useState(0);
  return (
    <div className="react-counter">
      <span className="vanilla-heading">
        {label}: {count}
      </span>
      <button
        className="mw-btn"
        onClick={handler(() => {
          const n = count + 1;
          setCount(n);
          emit('incremented', { count: n, label });
        })}
      >
        increment
      </button>
    </div>
  );
});

/**
 * 'notes' — the notes composer: a controlled input, a `useState` list, and
 * a 'noteAdded' emit per add. `value` is a wire prop — the driver writes it
 * back onto the real input, so clearing the draft clears the field.
 */
const NotesApp = islandApp('notes', function NotesApp({
  title = 'react island',
}: {
  title?: string;
}) {
  const [draft, setDraft] = useState('');
  const [notes, setNotes] = useState<string[]>([]);
  const add = (): void => {
    const text = draft.trim();
    if (text === '') return;
    setNotes([...notes, text]);
    setDraft('');
    emit('noteAdded', { text, total: notes.length + 1 });
  };
  return (
    <div className="react-notes">
      <h3 className="vanilla-heading">{title}</h3>
      <div className="atoll-map-places">
        <input
          placeholder="write a note…"
          value={draft}
          // _enrichEvent stamps the wire payload's `value` onto the target.
          onInput={handler((e) => setDraft(e.value ?? ''))}
          onKeyDown={handler((e) => {
            if (e.key === 'Enter') add();
          })}
        />
        <button className="atoll-map-place-btn" onClick={handler(add)}>
          add
        </button>
      </div>
      <ul className="vanilla-log">
        {notes.map((n, i) => (
          <li key={i} className="vanilla-log-line">
            {n}
          </li>
        ))}
      </ul>
      <div className="vanilla-readout">{notes.length} note(s) — state lives in the worker</div>
    </div>
  );
});

/**
 * 'incidents' — the heavy-component benchmark: 1,000,000 incident records
 * in the worker, rendered through a virtualized scroller. The main thread
 * only ever sees ~22 rows of ops no matter how deep the user scrolls.
 *
 * Rows are lazily generated (deterministic pseudo-data — nothing is
 * materialized until it's visible). Each scroll event is a dispatch
 * round-trip — the driver stamps `scrollTop` onto the payload — the worker
 * re-renders the window and reports the re-render time back via 'rendered'.
 */
const REGIONS = ['us-east', 'us-west', 'eu-central', 'ap-south', 'sa-east'];
const SEVS = ['P1', 'P2', 'P3', 'P4'];
const ROW_H = 24;
const OV = 4;
const VISIBLE = Math.ceil(320 / ROW_H) + OV * 2;
const incident = (i: number) => ({
  id: i,
  site: `site-${(i * 7919) % 1409}`,
  region: REGIONS[i % REGIONS.length],
  sev: (i * 31) % 100,
  dur: `${((i * 104729) % 977) % 60}m`,
});
const sevClass = (s: number): string =>
  `inc-sev sev-p${s > 75 ? 1 : s > 40 ? 2 : s > 15 ? 3 : 4}`;
const sevLabel = (s: number): string => SEVS[s > 75 ? 0 : s > 40 ? 1 : s > 15 ? 2 : 3];

const IncidentsApp = islandApp('incidents', function IncidentsApp({
  count = 1_000_000,
}: {
  count?: number;
}) {
  const [start, setStart] = useState(0);
  const [lastMs, setLastMs] = useState(0);
  const t0 = useRef(performance.now());
  const prevKey = useRef('');

  const first = Math.min(start, Math.max(0, count - VISIBLE));
  const rows = Array.from({ length: Math.min(VISIBLE, count - first) }, (_, k) =>
    incident(first + k),
  );
  const end = first + rows.length - 1;

  // Commit-phase report: the delta from the scroll handler is the whole
  // worker-side re-render cost. React commits inside the dispatch's sync
  // lane, so emit here is still in instance scope — no runInInstance
  // re-entry needed. The window-key guard stops lastMs's own write from
  // looping back into another report.
  useLayoutEffect(() => {
    const key = `${first}:${end}`;
    if (key === prevKey.current) return;
    prevKey.current = key;
    const ms = performance.now() - t0.current;
    setLastMs(ms);
    emit('rendered', { start: first, end, ms });
  });

  return (
    <div className="incidents">
      <div className="inc-stats">
        {count.toLocaleString()} incidents · rows {first.toLocaleString()}–
        {end.toLocaleString()} · worker re-render {lastMs.toFixed(1)}ms
      </div>
      <div
        className="inc-viewport"
        onScroll={handler((e) => {
          t0.current = performance.now();
          // The driver stamps the scroller's scrollTop onto the wire
          // payload — the proxy element's geometry getters are stubs.
          setStart(Math.max(0, Math.floor((e.scrollTop ?? 0) / ROW_H) - OV));
        })}
      >
        <div className="inc-spacer" style={{ height: count * ROW_H }}>
          {rows.map((r) => (
            <div key={r.id} className="inc-row" style={{ top: r.id * ROW_H }}>
              <span className="inc-id">#{r.id}</span>
              <span className="inc-site">{r.site}</span>
              <span className="inc-region">{r.region}</span>
              <span className={sevClass(r.sev)}>
                {sevLabel(r.sev)} · {r.sev}
              </span>
              <span className="inc-dur">{r.dur}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
});

export const reactWorker = defineReactPolyWorker({
  apps: { counter: CounterApp, notes: NotesApp, incidents: IncidentsApp },
});

The incidents island's contract module is the whole lazy story: a shell-safe module that names the registry key AND carries the worker factory — the shell-side lazyIsland resolves both, so the benchmark's worker is a bundler-detectable split point.

examples/react-dom-worker/src/incidents.island.tsfrontend
/**
 * Island contract for the `lazyIsland` facade — resolves `{ app, worker }`
 * so the island carries its own worker factory and the call site needs no
 * `worker` prop at all. The dynamic `import('./incidents.island')` in
 * shell.tsx is the bundler's split point: this module (and anything it
 * pulls) only loads when the island mounts.
 *
 * `app` is the registry key as a STRING — importing the component itself
 * would drag worker-side code into the shell bundle; the name is the
 * whole contract.
 */
export const app = 'incidents';
export const worker = (): Worker =>
  new Worker(new URL('./worker/react.worker.tsx', import.meta.url), { type: 'module' });

Notes

  • Mediation is unidirectional — an island's emit lands in onEvent, the shell sets state, and it flows back in as props. No hand-wired updateProps calls.
  • Two fallback phases: <Suspense> for the module load, the proxy's fallback prop for the worker mount.
  • React commits in the dispatch's sync lane — unlike the other frameworks' async schedulers, useLayoutEffect fires while the task still holds the instance, so commit-phase emitneeds no runInInstance re-entry. That's how the incidents benchmark reports its re-render time from a layout effect.
  • Fixed-dimension libs (recharts) get width/height as props — there's no ResizeObserver channel into the worker; measurement reads on the proxy DOM return zero.
  • Structural walls stay walls: closed libraries that need real DOM (Google Maps JS) are housed via transclusion slots or iframe elements — the worker owns the box, the shell owns the contents.