PraxisJS

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/jsx
pnpm add @praxisjs/jsx
yarn add @praxisjs/jsx
bun add @praxisjs/jsx

The 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>
  )
}
Storybook
Live demo — reactive vs. static

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().

Storybook
Live demo — lists

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>
  )
}
Storybook
Live demo — event handlers

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?

On this page