An experimental fine-grained reactive web UI framework for Koka, built on algebraic effect handlers.
Effx implements Solid.js style reactivity using effect handlers instead of a hand written runtime. Dependency tracking, subscriptions, and ownership all arise from a small set of effect operations. Reading a reactive cell captures a continuation and writing to the cell resumes it. There is no virtual DOM and no diffing. When a value changes, only the narrowest computation that observed it runs again, down to a single attribute or a single text node.
state<a>is a reactive cell with a subscriber list.- The
reacteffect providesgetandset.get(s)is actloperation: the handler stores the captured resumption ons's subscriber list, so a laterset(s, v)re-runs exactly the computation that read it. Dependencies are tracked per handler installation and re-diffed on each fire, the algorithmic analog of Solid's sources array. - The
ownereffect gives explicit ownership scopes. Everyhandle-reactinstallation registers a self-disposer with the ambient owner, so disposing an owner tears down all subscriptions created inside it (used by conditional and list rendering to clean up replaced subtrees). - Rendering wraps each attribute and each child in its own
handle-react, which is what makes updates fine-grained.
| Module | Purpose |
|---|---|
effx/effx.kk |
Reactive core: state<a>, the react and owner effects and their handlers |
effx/html.kk |
html / attr data types and the render walk that produces live DOM nodes |
effx/dsl.kk |
Builder DSL: write HTML trees as indented blocks (div, p, text, branch, each, ...) |
effx/bindings.kk |
Raw extern JS/DOM bindings everything else is built on |
module counter
import bindings
import effx
import html
import dsl
pub fun app() : <io, react, owner, html-builder> ()
val count = state/new("0")
div
h1
text-lit("Effx counter")
p
text-lit("Count: ")
text(count)
button(fn()
val n = (!count.item).parse-int.default(0)
count.set((n + 1).show)
)
text-lit("+1")
pub fun main() : <io> ()
val root = get-element-by-id("root")
root.append-child(mount(app))mount installs the top-level owner and react handlers, runs the build-tree to assemble the html value, and renders it to a DOM node. text(count) subscribes that one text node to the cell, so clicking the button updates only it.
Compile an example to JavaScript (requires Koka, tested with v3.2.3):
koka --target=js -c -ieffx -iexamples examples/counter.kkPoint examples/main.js at the generated <module>__main.mjs under examples/.koka/, then serve from the project root and open the page:
python3 -m http.server 8000
# open http://localhost:8000/examples/Examples:
examples/counter.kk— counter with a two-way-bound name inputexamples/card-demo.kk— a reusable component that accepts indented childrenexamples/sheet.kk— a spreadsheet: formulas with ranges and functions, selection, undo/redo, copy/paste, drag-fill, column resize/sort/filter, find/replace
MIT — see LICENSE.