React in a cell

A cell is a scene's living unit — a view (its canvas, the outward face)

plus data (its interior). The canvas is a self-contained HTML document, which

is enough to run React from a CDN with no build step — and inside a scene the two

things an external app has to wire up are already there: the data is injected

and auth is ambient. So a React cell is often shorter than the external

version.

What the runtime hands you

The renderer injects these globals beside your HTML — you never bake data or

credentials into the document:

GlobalWhat it is
window.__DASLAB_WIDGET_DATA__This canvas's fields.data, inlined for a synchronous first render — no round-trip.
window.__DASLAB_WIDGET_CTX__Scene id, asset id, host, and a scoped, viewer-bound session.
window.daslab / window.daslabReadyThe typed client factory. d.call('<tool>', input) runs a tool as the signed-in viewer — no token to pass.

Because the session is viewer-bound, a canvas rendered for a signed-in viewer

can call tools; a display-only render (a public snapshot) can't — it throws a

clear "display-only" error rather than leaking anything.

The pattern

Pin production builds; never @latest (it's a redirect with no long cache and

can break under you). Avoid in-browser Babel — write hyperscript or ESM, not

JSX-compiled-at-runtime; the transform is the one real performance killer in a

cell.

<div id="root"></div>
<script type="module">
  import React from "https://esm.sh/react@19";
  import { createRoot } from "https://esm.sh/react-dom@19/client";
  const { useState, useEffect } = React;
  const h = React.createElement;

  function Bom() {
    const [data, setData] = useState(window.__DASLAB_WIDGET_DATA__ ?? null);
    useEffect(() => {
      (async () => {
        const d = (await window.daslabReady)();
        const r = await d.call("parts_configure", { use_case: "machine-monitoring", scale: 12, market: "th" });
        setData(JSON.parse(r.result.content[0].text));
      })();
    }, []);
    if (!data) return h("p", null, "configuring…");
    return h("div", null,
      h("h1", null, data.total_display),
      h("small", null, data.per_unit_display),
      h("a", { href: data.share_url, target: "_blank" }, "open configurator ↗"),
    );
  }

  createRoot(document.getElementById("root")).render(h(Bom));
</script>
  • useState(window.__DASLAB_WIDGET_DATA__) renders committed data instantly,
then the effect fetches fresh — so the tile is never blank.
  • d.call(tool, input) is the same tool surface the agent tools and the
external SDK use. Multi-account tools take an accountId in the input.
  • One esm.sh URL per lib, version pinned. esm.sh resolves peer deps, so you
don't hand-wire React into ReactDOM.

JSX, if you want it

Skip Babel-in-the-browser. If you want JSX ergonomics, either write hyperscript

as above, or author the cell as a real React app in your own toolchain and paste

the built output — the injected globals are identical either way. Which is the

point: the same component runs in a canvas and in an external app.

When to reach for the SDK hooks

The manual useEffect + d.call bridge above is fine and dependency-light. The

@daslab/sdk React hooks (useDaslabCall, useScene, <Widget/>) wrap that bridge with caching, dedup, and refetch, and let a

canvas and an external app share one identical component. <Widget data={…}/>

is pure — it renders any widget payload with no transport — so it drops into a

cell today over d.call results.

Updated 2026-07-23