JSX Syntax
PraxisJS uses a custom JSX runtime. Learn how to write reactive templates, handle events, use fragments, and map lists.
JSX Syntax
PraxisJS uses a custom JSX runtime (@praxisjs/jsx). Files with JSX must use the .tsx extension.
npm install @praxisjs/jsxpnpm add @praxisjs/jsxyarn add @praxisjs/jsxbun add @praxisjs/jsxThe one rule
Arrow functions are reactive, plain expressions are static.
render() {
return (
<div>
{() => this.name} {/* ✅ reactive — updates on signal change */}
{this.name} {/* ❌ static — read once at render time */}
{() => this.count * 2} {/* ✅ reactive expression */}
{() => this.active ? 'on' : 'off'} {/* ✅ reactive conditional string */}
</div>
)
}Conditional rendering
Use an arrow function so the condition re-evaluates when the signal changes:
render() {
return (
<div>
{/* Renders or removes <Modal /> when isOpen changes */}
{() => this.isOpen && <Modal />}
{/* Ternary — swaps between two components */}
{() => this.loading ? <Spinner /> : <Content />}
</div>
)
}Lists
Render arrays inside an arrow function so the list rebuilds when the signal changes:
render() {
return (
<ul>
{() => this.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}No reconciliation
PraxisJS does not reconcile lists. When the arrow function re-runs, the entire list is replaced — all previous nodes are removed and new ones are inserted. The key prop is accepted by the type system for forward compatibility but has no runtime effect. For large lists, use the VirtualList composable instead of a plain .map().
Event handlers
Event props use camelCase (onClick, onInput, onKeyDown, onPointermove, etc.). Event handlers are plain arrow functions — they don't need to be reactive:
render() {
return (
<div>
<button onClick={() => this.count++}>Increment</button>
<input
onInput={(e) => {
this.value = (e.target as HTMLInputElement).value
}}
/>
</div>
)
}CSS classes
Static class:
<div class="card elevated">...</div>Reactive class (rebuilds when the signal changes):
<div class={() => this.active ? 'card active' : 'card'}>...</div>Inline styles
Static:
<div style={{ color: 'red', fontSize: '16px' }}>...</div>Reactive:
<div style={() => ({ opacity: this.visible ? 1 : 0, transform: `scale(${this.scale})` })}>
...
</div>Fragments
Render multiple elements without a wrapper node:
render() {
return (
<>
<Header />
<Main />
<Footer />
</>
)
}ref — accessing DOM elements
Use @Ref<T>() from @praxisjs/decorators to declare a typed DOM ref. The decorated field is both the ref callback and the holder of the element:
import { Component, Ref } from '@praxisjs/decorators'
@Component()
class InputFocus extends StatefulComponent {
@Ref<HTMLInputElement>()
inputRef!: Ref<HTMLInputElement>
onMount() {
this.inputRef.current?.focus()
}
render() {
return <input ref={this.inputRef} />
}
}Ref<T> is callable (used as the ref prop) and holds the element in .current after mount. The callback fires once when the element is inserted into the DOM — it is not called again on unmount.
For refs outside of a class — module-level constants or options like root in @Lazy — use createRef<T>():
import { createRef } from '@praxisjs/decorators'
const scrollRoot = createRef<HTMLDivElement>()
// pass to @Lazy({ root: scrollRoot }) or use in JSX:
<div ref={scrollRoot}>...</div>You can also store the element directly with a plain callback when you don't need a ref object:
render() {
return <input ref={(el) => { this.inputEl = el }} />
}ref — accessing component instances
The same ref prop works on component tags. The callback receives the component instance after onMount completes, and null after the component unmounts.
@Component()
class Parent extends StatefulComponent {
private modal: Modal | null = null
render() {
return (
<>
<button onClick={() => this.modal?.open()}>Open</button>
<Modal ref={(inst) => { this.modal = inst }} />
</>
)
}
}Use ComponentRef<C> from @praxisjs/jsx to type the field explicitly:
import type { ComponentRef } from '@praxisjs/jsx'
@Component()
class Parent extends StatefulComponent {
private modal: ComponentRef<typeof Modal> = null // (instance: Modal | null) => void ... typed field
// or just store the instance:
private modalInstance: Modal | null = null
render() {
return <Modal ref={(inst) => { this.modalInstance = inst }} />
}
}ComponentRef<C> resolves to (instance: InstanceType<C> | null) => void, so the callback argument is fully typed to the component class.
Timing
The ref callback fires in a microtask after onMount has run on the child component. If you need to call a method on the instance immediately after mount, do it inside onMount of the parent — the child will already be mounted by then.
ref is not forwarded
The ref prop is stripped by the runtime before the component receives its props. The child component never sees it — it is handled entirely by the parent's rendering scope.
What's next?
- Lifecycle Hooks — when
onMountfires and how to clean up - Reactivity & Signals — the full signal model, computed values, and reading without tracking
- Decorators: Events & Slots —
@Emit,@Slot, and@OnCommand
Reactivity & Signals
PraxisJS uses signals for fine-grained reactivity — only the specific DOM nodes that depend on a changed signal are updated.
Lifecycle Hooks
PraxisJS components expose four lifecycle hooks — onBeforeMount, onMount, onUnmount, and onError — available on both StatefulComponent and StatelessComponent.