@webfront/react is an optional package, installed by wf from an npm-compatible
registry. Its sources and declarations live in js/react; it is not an embedded
core resource. React remains a peer dependency, shared with the application. @types/react is an
optional peer for TypeScript projects; install it explicitly alongside TypeScript.
No react-reconciler is used. The peer ranges are React ^19.3.1 and optional
@types/react ^19.3.0; CI tests React 19.3.1 and @types/react 19.3.0. useSyncExternalStore reads cached snapshots;
development React 19.3.1 is exercised under StrictMode. This lower bound is a
support/qualification policy based on that CI baseline, not a technical requirement
of the hooks: the hooks used by the adapter exist in React 18, but React 18 and
19.0–19.2 have not been qualified here.
Install the browser receiver before asking C++ to send:
import {createStore, useStore} from '@webfront/react';
const statistics = createStore<[number, string]>('statistics', [0, 'Waiting']);
function Statistics() {
const [count, label] = useStore(statistics);
return <p>{count}: {label}</p>;
}In a server-context callback, construct and retain a store for that browser:
#include <webfront/components/react/Store.hpp>
using Snapshot = std::tuple<std::uint32_t, std::string>;
webfront::react::Store<UI, Snapshot> statistics{ui, "statistics"};
statistics.set({1, "Running"});
statistics.set({2, "Running"}); // replaces the pending snapshotCopies share pending state. Moving a store transfers its wrapper: the source's
valid() returns false, and set, flush, close and onError throw
std::runtime_error on that moved-from wrapper. The first set schedules an application-owned task,
which waits 1 ms on the server context. Only the last complete snapshot is sent.
A burst of 1,000 set calls in one callback produces one bridge message and one
React commit in the browser regression. This is a server cadence, not a rendering
deadline: writes in later cycles can cause additional commits. Different stores
have independent cycles; fields requiring an atomic update belong in one tuple.
flush() queues the latest snapshot early, without awaiting the browser.
T must be a supported bridge value: scalar, string, tuple, numeric typed array
or packed bitset. Tuples arrive as arrays. Ordinary JavaScript objects do not cross
the bridge. Snapshot identity is stable until a message arrives; treat snapshots,
including nested arrays and typed arrays, as immutable. Subscribe functions stay
stable and return an unsubscribe function. An identical snapshot by Object.is
does not notify. Store names contain ASCII letters, digits, _ or -. The browser namespace
webfrontReactStores is reserved for these receivers. A throwing subscriber is
logged to the browser console and does not interrupt the remaining subscribers;
the adapter contains diagnostic logging failures too.
The C++ store shares its connection's document lifetime and thread rules, even
when no island is used. It does not require dom.connect() to send data. Create
and mutate it on the server context. Disconnect, document rejection and application
shutdown invalidate it; the timer is owned and drained by the application.
valid() returns false off that thread. Retained stores are safe to inspect after
the application has been destroyed; they do not extend the lifetime of UI.
close() discards pending values, releases the installed error callback and
invalidates all copies. Synchronous errors
from set/flush throw. An explicit flush() encoding/send failure retains the
pending snapshot and leaves the store open for retry (including the scheduled
cycle). Only a successful send clears that pending snapshot. An automatic
encoding/send failure closes the store and calls onError(std::exception_ptr) if
installed. The automatic failure consumes and clears that callback before invoking
it, so a callback capturing a store copy does not keep a closed store alive.
Self-owning callbacks still require an explicit close() on the normal path;
prefer weak captures when that lifecycle cannot be guaranteed.
Exceptions thrown by onError are caught and logged; they cannot escape
the automatic-send coroutine. Browser function failures are
not acknowledged by this untyped, fire-and-forget C++ call.
Create only one C++ store per browser/name and share its copies; two independently
constructed stores do not coalesce together. Browser createStore rejects duplicate
names. dispose() removes the receiver and subscribers; coordinate it with C++
close() after stopping writers. Store disposal is explicit, independent of a
component's StrictMode cleanup. Unmount all consumers before disposal: subscribing
to a disposed store throws, including a later useSyncExternalStore resubscription
while rendering a mounted consumer. Optional {target: window} installs a receiver
in another window; the default is globalThis.
Load the optional server interpreter and pass it to an empty component:
import {Island} from '@webfront/react';
async function prepareDom() {
const interpreterUrl = '/webfront-dom.mjs';
const {installDomInterpreter} = await import(interpreterUrl);
return installDomInterpreter(webFront);
}
// After prepareDom() resolves, render inside your existing React tree:
// <Island dom={dom} name="panel" className="panel" />Island renders a div without children. Its layout effect lends that element;
after the React commit, await dom.connect() and ask a C++ callback to build
ui.document().island("panel"). Never send mutations before the corresponding
island is committed. children and dangerouslySetInnerHTML are rejected at
runtime and excluded from its TypeScript props. Keep name and dom stable for
the mounted component. Each island name is unique within its interpreter.
StrictMode's immediate effect cleanup/setup replay keeps the same loan and C++
children. Release is deferred to a microtask; a real unmount revokes the loan.
Unexpected synchronous release errors are caught and logged, and the adapter drops
its local lease. Such an error still requires application-level recovery/reload;
logging it does not acknowledge a C++ revocation. A bridge already closed rejects
the interpreter's revocation Promise, which the interpreter handles and logs.
Parent renders preserve the C++ subtree. A later real mount gets fresh node IDs.
Delayed effect disconnect/reconnect mechanisms such as React
Activity or Offscreen are unsupported
for a live Island: hiding revokes its loan, and revealing preserved DOM does not
restore it. Coordinate a full unmount, fresh empty mount and C++ rebuild instead.
Unmount still requires the document handoff:
pause all document writers, flush, await a browser round trip, then unmount and
await an application-level acknowledgement that C++ observed the revocation
before resuming. The release function itself returns no acknowledgement or Promise.
The component cannot pause application writers itself. An uncoordinated unmount can close the whole document.
examples/life/react/ keeps the simulation in C++, renders the grid and statistics
with React, and builds a heading, status and “Next generation” button in C++ inside life-panel.
The C++ event computes a generation, updates the island and sets a tuple snapshot
containing the packed grid, generation and population. Each browser owns a separate
simulation, released on disconnect. The original Life starter remains available.
Build the WebFrontLifeReact CMake target. The application only creates an
empty project directory when it is missing (without argument ./life-project);
it does not copy the example's sources. Use a new directory, so that no existing project is touched: mkdir fails if it exists and cp -n never overwrites a file. Copy main.tsx and
webfront.project.json from examples/life/react/ into that directory first,
then start the application:
mkdir life-react-project
cp -n examples/life/react/main.tsx examples/life/react/webfront.project.json life-react-project/
./build/examples/WebFrontLifeReact life-react-projectIn its terminal, run wf install, then wf preview. The configured registry must
carry @webfront/react@0.1.0; this change prepares the archive but does not publish
to npm. The repository's test registry already carries the package. For a local
evaluation, configure the driver with --registry-dir <build>/test/web/self-build-project/build-registry;
the browser suite uses that registry for this adapter and React. To use the same
registry in WebFrontLifeReact, copy its contents to <project>/registry/ and add
"registry": "/registry/" to the project manifest before wf install. This is a
development mirror served from the project document root.
To prepare the archive, use the repository wf project after restoring its packages:
wf run react-packageThis writes build-react-package/webfront-react-0.1.0.tgz and integrity.txt.
Publishing that archive to a registry is a separate maintainer action.
The test registry uses the same deterministic packager as wf run react-package.
ReactAdapterPackageContents verifies byte-for-byte archive equality, the exact
file list, every maintained source file and the SHA-512 in integrity.txt.
The Life canvas ignores clicks until life.react.start acknowledges startup. If
the React island does not mount within five seconds, startup fails visibly.
Editing a cell starts a new simulation at generation zero.
The generated .tgz uses valid gzip stored blocks, without size compression, so
bootstrap packaging needs no preinstalled compressor. Registry publication is a
separate step and may retain this format or recompress with a newly recorded digest.
The canvas cell editor is pointer-only; the C++ “Next generation” button supports
normal keyboard activation. A complete keyboard cell editor is not demonstrated.