Skip to content
Logo

freact API

Everything you can write in freact, and nothing else. The surface is small on purpose: each export exists because some encapp construct is its image, and anything with no image is a TypeError at construction or a freact-check failure at build.

The one rule that outranks this page: a program is freact iff freact-check exits 0 on it. If this page and the checker ever disagree, the checker is right.

node tools/freact-check.mjs --summary src/**/*.tsx     # exit 0 ⟺ freact

Building a view

el(tag, props?, ...kids)

An element node.

import { el, text, on } from 'freact'
 
el('div', { class: 'row', style: 'display:flex;gap:12px' },
  el('span', null, 'Alice'),
  el('button', { onClick: on('openProfile', { pub: 'ab12…' }) }, 'Open'))
  • tag is an intrinsic tag name and must be a string. A computed tag throws: "a computed tag has no image in RView". RView elements are constructors, not values.
  • props are attributes, plus key, plus on* handlers built by on(). undefined, null and false values are skipped, so cond && { … } spreads are unnecessary.
  • kids may be nodes, strings, numbers, or arrays thereof; null/false vanish.

text(v)

A text node. null/undefined become the empty string.

fragment(view)

Flattens a view — or a list of them — into the nodes it denotes. A view is allowed to be a list of siblings: RView holds List RView, and a compose bar is a strip plus a bar with no wrapper element between them. Both backends normalize the root through this, so the two agree on what a list root means.

Events — the part that carries the bound

on(tag, payload?)

A serializable event binding: which effect fires, and with what payload.

el('button', { onClick: on('saveProfile') }, 'Save')
el('li',     { onClick: on('openChat', { with: row.pub }) }, row.name)

tag must be a non-empty string. The payload is checked for serializability at construction, not at dispatch — pass a function or a DOM node and you get a TypeError immediately.

This is the whole reason freact is not React with lint rules. onClick={() => doThing(x)} is a closure, and a closure has no image in Eff. on('doThing', { x }) is data: a name and a payload, which is exactly what an Eff constructor is.

stop()

A binding that only stops the event travelling further — no effect fires.

el('div', { class: 'backdrop', onClick: on('closeDialog') },
  el('div', { class: 'card', onClick: stop() }, /* … */))

This exists because a modal card must swallow clicks that would otherwise reach the backdrop and close the dialog. React wrote that as onClick={(e) => e.stopPropagation()} — an inline body whose entire content is event routing, not app logic. Routing is a property of the binding, so it is declared as one: still data, still serializable, still no closure.

A binding may also carry stop, prevent and key — the routing facts React expressed as statements inside a handler body.

Styles

style(obj)

Serializes a style object to a CSS string using React's exact separator discipline — a:1px;b:2px, no spaces, no trailing semicolon. That is measured against react-dom/server, not assumed: the pretty form would make every node in a port differ.

Empty, null, undefined and boolean values are dropped.

The value vocabulary

freact/vocab gives constructors for the values Encapp.Style can hold. Using them is what makes a style a value rather than a string:

constructorproduces
px(n)"12px"
pct(n)"50%"
len(v)a length, from a number or an already-valid string
box(v)a box shorthand (1–4 values)
colour(c)a colour
background(v)a background — colour or gradient
border(w, c)"1px solid #E5E5E5"
count(n)a non-negative integer as a string
align(v) · justify(v) · textAlign(v)the flex/text enums, defaulted rather than thrown

CONSTRUCTORS exports that list, so a tool can enumerate the vocabulary rather than hard-code it.

Rendering

mountDom(container, { onEvent })

Mounts into a real DOM node. Returns a renderer whose render(view) diffs and patches.

Events see the tree they were dispatched into. A handler may commit a new view — that is the point of a handler — and this renderer reuses DOM nodes across a commit. So a commit bumps a generation and the event sink stamps each event on first entry, ignoring it once the generation moves. Without that, a reused ancestor carrying the next page's binding could fire for an event still travelling from the previous tree.

mountHeadless(_container?, { onEvent }) and toHtml(node)

The same renderer with no DOM. toHtml serializes a node to markup — this is what the 224-fixture differential compares against renderToStaticMarkup.

renderRView(doc, node, row) · renderRViewAll(...)

Renders encapp's RView protocol shape directly. This is the bridge in the running direction: Lean emits RView, and this interprets it with the same renderer that interprets hand-written freact.

Host capabilities

Everything a component may not reach ambiently is declared instead.

export const CAPABILITIES = ['clock', 'intl', 'env', 'blobs', 'capture', 'ask']

checkHost(host, required?) validates a host against that set and returns it, so construction can be wrapped in the check. It throws when a declared capability is missing or malformed — an undeclared or half-implemented world should not start — rather than failing later at the first call.

Three hosts ship: browserHost, memoryHost, serverHost.

This is where the security argument lands in the API: a component cannot read window or call fetch, so the set of things an app can reach is this list, and it is enumerable at build time rather than discovered by running the program.

What has no image, and what to write instead

you would write in Reactwrite in freactwhy
useStatea Doc scalar, set by an effectDoc scalars are typed Atoms; useState holds any JS value
useEffecta declared command on a node (cmd)Eff has no "later, imperatively"
onClick={() => f(x)}onClick={on('f', { x })}a handler must name an effect with a serializable payload
onClick={(e) => e.stopPropagation()}onClick={stop()}routing is a property of the binding
useRef + node accessa command on the nodeRView is data, not a handle
window / localStorage / fetcha host capabilityrender must be a function of Doc alone
<Tag> where Tag is computeda conditional over fixed tagselements are constructors, not values
{...props}named propsa spread hides which fields cross the boundary
useContextan explicit propa dynamic scope no first-order view can name
a class componenta functionlifecycle has no image in Doc/Eff

Each row is enforced by a named rule; run the checker to see which one fires and where.