skip to the page

Rendering a tree

Rendering turns a tree into React elements. It is synchronous, pure, and it never fails.

import { createThemeRegistry } from "@jam-overture/loom"
import { createStarterPrimitiveRegistry } from "@jam-overture/loom-primitives"
import { renderLoomTree } from "@jam-overture/loom/react"

const registry = createStarterPrimitiveRegistry()

if (!registry.ok) throw new Error("the registry was refused")

const rendered = renderLoomTree(tree, {
  resolver: registry.value,
  validator: registry.value,
  themes: createThemeRegistry(),
})

return <main>{rendered.element}</main>

Three options and one of them is required. resolver answers "what component is loom.heading?". validator answers "are these props legal for it?". A registry built through the SDK satisfies both, so passing the same object twice is the ordinary case — they are separate options because they are separate questions, and a host may want the first without the second.

It always returns an element

This is the property to plan around. A tree can name a primitive you have not registered — a proposal invented it, or a deployment rolled the code back but not the tree — and blanking a page over one unknown card serves nobody.

So renderLoomTree renders what it can and reports what it could not, beside the element rather than instead of it:

const { element, diagnostics, theme } = renderLoomTree(tree, options)

diagnostics is a list, and it is empty on a healthy render. Each entry names the node it happened at:

CodeWhat happened
unknown-primitivethe tree named a type the resolver does not have
invalid-propsprops failed the primitive's schema; the node rendered without them
props-undeclaredthe resolver knows the type and the validator does not — a wiring fault
theme-unresolvedthe root named a theme the registry refused

Themes are three ids on the root

A theme is not a stylesheet you import. It is a palette, a font pack and a style preset, named by id on the root node under the reserved loom:theme prop:

props: {
  "loom:theme": {
    palette: "minimal",
    fontPack: "minimal-sans",
    stylePreset: "precise",
  },
}

Pass a ThemeRegistry and the render resolves those three ids and hands the result to the root primitive, which mounts them as CSS custom properties on its own element. Every primitive underneath is styled from var(--loom-*) and never knows which theme it is wearing.

That is worth reading twice, because it is the one part that catches people out: mounting is the root primitive's job, and loom.page is the primitive in the starter library that does it. A tree rooted at something else renders with none of those properties set — legal, silent, and indistinguishable from a stylesheet that failed to load.

The same nodes, three different idslive · rendered through the runtime

Hello from a tree

Nothing here was written as markup.

Propose a change:

Every one of these goes through the same pipeline a model’s answer would: interpreted, analysed, weighed, judged, applied, appended. The log is real and it is in this tab — reload the page and the example is back as it was.

show the tree
{
  "kind": "element",
  "id": "n_themedtree5",
  "type": "loom.page",
  "props": {
    "fills": true,
    "width": "readable",
    "loom:theme": {
      "palette": "bold",
      "fontPack": "bold-sans",
      "stylePreset": "airy-modern"
    }
  },
  "children": [
    {
      "kind": "element",
      "id": "n_themedtree2",
      "type": "loom.heading",
      "props": {
        "level": 1
      },
      "children": [
        {
          "kind": "text",
          "id": "n_themedtree1",
          "value": "Hello from a tree"
        }
      ]
    },
    {
      "kind": "element",
      "id": "n_themedtree4",
      "type": "loom.prose",
      "props": {},
      "children": [
        {
          "kind": "text",
          "id": "n_themedtree3",
          "value": "Nothing here was written as markup."
        }
      ]
    }
  ]
}
Identical to the tree on “Your first tree” but for the palette, font pack and style preset named on its root. No component was touched.

Two consequences follow, and both are the reason it is done this way. A re-theme is an ordinary configure on one node — the same operation as any other change, gated the same way. And the registry you pass is the allowlist: a proposal can only name a palette you registered, so "make it look different" has a bounded set of answers.

Serving a page, rather than rendering a value

renderLoomTree is synchronous because rendering is. A real request usually has two things to do first — load the tree from somewhere, and answer any data bindings it declares — and both are IO.

renderRequest is where those meet: it loads, parses, plans the tree's bindings, resolves them all at once, and then renders. It returns a Result, because loading and parsing are the two steps that genuinely can fail.

const served = await renderRequest(request, { source, resolver, validator, themes })

if (!served.ok) return notFound()

return <main>{served.value.element}</main>

Rendering stays as pure as it was; the IO happens in one place, before the walk.