Cross-Platform Rendering
Write once, render to DOM, Canvas 2D, React Native, HTML (SSR), and more. Same reactive logic, different render targets.
A reactive rendering framework agnostic to both reactive systems and rendering targets.
import { Signal } from 'signal-polyfill'
import { com } from '@rasenjs/core'
import { useReactiveRuntime } from '@rasenjs/reactive-signals'
import { mount } from '@rasenjs/dom'
// Install the reactivity adapter once, at startup.
useReactiveRuntime()
// A component is a function that returns a mountable. `com()` gives each
// instance its own effect scope, so its subscriptions stop on unmount.
const Counter = com(() => {
const count = new Signal.State(0)
return (
<div class="counter">
{/* a bare ref child is bound by the host as a reactive text node */}
<p>Count: {count}</p>
<button onClick={() => count.set(count.get() + 1)}>+</button>
</div>
)
})
mount(<Counter />, document.getElementById('app')!)import { Signal } from 'signal-polyfill'
import { useReactiveRuntime } from '@rasenjs/reactive-signals'
import { canvas, mount } from '@rasenjs/dom'
import { rect, text } from '@rasenjs/canvas-2d'
useReactiveRuntime()
const x = new Signal.State(50)
// The canvas bridge lives in @rasenjs/dom; the shapes come from the target.
mount(
canvas({
width: 400,
height: 200,
children: [
rect({ x, y: 50, width: 100, height: 80, fill: '#4CAF50' }),
text({ text: 'Rasen', x: 12, y: 24, fill: '#333' })
]
}),
document.getElementById('app')!
)
// Shape props are reactive: writing the signal marks the canvas dirty.
const start = performance.now()
const spin = (now: number) => {
x.set(150 + Math.sin((now - start) / 500) * 100)
requestAnimationFrame(spin)
}
requestAnimationFrame(spin)import { renderToString, div, p, ul, li } from '@rasenjs/html'
// Same components, string host — no DOM required.
const html = renderToString(
div(
{ class: 'container' },
p({ class: 'title' }, 'Hello from SSR!'),
ul({ class: 'list' }, li('Item 1'), li('Item 2'))
)
)import { View, Text, TouchableOpacity, registerApp } from '@rasenjs/react-native'
import { useReactiveRuntime } from '@rasenjs/reactive-vue'
import { ref } from '@vue/reactivity'
useReactiveRuntime()
const count = ref(0)
registerApp('MyApp', () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ fontSize: 48 }}>{count}</Text>
<TouchableOpacity onPress={() => count.value++}>
<Text>+</Text>
</TouchableOpacity>
</View>
))What this needs
jsxImportSource in tsconfig.json ("@rasenjs/dom" here) and the three packages above — the whole example is JSX, com() and TC39 signals. Adding @rasenjs/compiler is optional: it hoists static subtrees and turns .map() / cond && <El/> in a child position into each() / when() (JSX & the Compiler).
Everything in Rasen comes down to one simple type:
type Mountable<Node = unknown> = (
node: Node,
hooks: HostHooks<Node> | undefined
) => (() => void) | undefinedA component receives a host, mounts itself into it, and returns a cleanup function. That's it — no virtual DOM, no component instances.