JSX runtime API reference

June 5, 2026 · View on GitHub

The functions the in-VM JSX runtime exposes to plugin code (jsx: true). Where they land in plugin scope — zushi.useState, bare, custom — is up to the host; see placement. All entries below are also importable from @reearth/zushi/jsx for bundled plugins.

Behavior is React-like but VM-local: reconciliation and hooks run inside the VM, and only a serialized tree crosses to the host (see overview).

Elements

createElement(type, props?, ...children) · h

Create a virtual node. type is an intrinsic tag name (string), a component function, or Fragment. h is an alias. Returns a VNode.

h("div", { className: "row" }, h("span", null, "hi"))

Fragment

Groups children without a wrapper element. It's the literal string "__zushi.Fragment" (so it survives marshaling/bundling), used as a type.

Surfaces

surface(name?)

Look up a surface handle by name; with no argument it returns the host's default surface ("ui" when declared, else the only one). Throws on an unknown name. Each declared surface is also exposed directly by name — zushi.ui, zushi.modal, … — so surface() is mostly for dynamic lookup.

Surface handles — ui.render, ui.resize, ui.show, ui.hide

ui.render(element, {
  visible?: boolean,
  width?: number | string,
  height?: number | string
})
ui.resize(width?, height?)
ui.show()
ui.hide()

render mounts an element tree into the surface and starts reconciling it. resize / show / hide drive the mounted surface afterwards (with a host-direct renderer, visibility is the host's call and only resize is honored).

State

useState(initial)

const [value, setValue] = useState(initial /* value | () => value */);
setValue(next);            // value | (prev) => next

VM-local, per-component, ephemeral. Updates re-render when the value changes (compared with Object.is).

useReducer(reducer, initialArg, init?)

const [state, dispatch] = useReducer((state, action) => next, initialArg, init?);

useRef(initial)

const ref = useRef(initial); // { current }, stable across renders, no re-render on change

Synced state — useSyncedState, useSyncedMap

State that lives in the host-owned store instead of VM memory (shared across surfaces, host-drivable, optionally persisted). Full docs: Synced state.

const [n, setN] = useSyncedState("count", 0);
const map = useSyncedMap("items"); // get/has/set/delete/keys/values/entries/size

Effects

useEffect(effect, deps?)

useEffect(() => {
  // run after render (when deps change, or every render if deps omitted)
  return () => { /* cleanup before next run / on unmount */ };
}, [a, b]);

Deps are compared with Object.is.

useLayoutEffect(effect, deps?)

An alias of useEffect — there's no separate layout phase in the VM.

Memoization

useMemo(factory, deps?) · useCallback(cb, deps?)

const value = useMemo(() => compute(a), [a]);
const cb = useCallback(() => doThing(a), [a]); // === useMemo(() => cb, deps)

memo(Component, areEqual?)

Wrap a component so it re-uses its last render when props are shallow-equal (and its own state hasn't changed). areEqual(prev, next) overrides the comparison.

Context

createContext(defaultValue) · useContext(context)

const Ctx = createContext(defaultValue);
ui.render(h(Ctx.Provider, { value }, children));
const value = useContext(Ctx); // nearest enclosing Provider value, else default

Misc

useId()

Returns a stable, per-component-instance id string (for form/aria wiring).

Components

ErrorBoundary({ fallback, onError?, children })

Catches errors thrown while rendering its subtree and shows fallback (a node or (error) => node) instead; onError(error) is called when it catches.

Suspense({ fallback, children })

Shows fallback while a child throws a thenable, then re-renders when it settles. There is no lazy() — the VM has no module loader.

components

The record of trusted components the host registered, keyed by name — destructure what you need, Figma-style:

const { View, Text } = zushi.components;
ui.render(h(View, null, h(Text, null, "hi")));

registerComponent(name, fn)

Registers a trusted custom component into the components record. Sealed from plugin code by default — only the trusted setup slot can call it. See Components & intrinsics.