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:
| Global | What 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.daslabReady | The 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,
d.call(tool, input)is the same tool surface the agent tools and the
accountId in the input.
- One esm.sh URL per lib, version pinned. esm.sh resolves peer deps, so you
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