@praxisjs/decorators
npm install @praxisjs/decoratorspnpm add @praxisjs/decoratorsyarn add @praxisjs/decoratorsTypeScript decorators for defining components, reactive state, lifecycle hooks, and utilities.
Component Decorator
@Component()
Marks a class as a PraxisJS component.
import { Component, Slot } from '@praxisjs/decorators'
import { BaseComponent } from '@praxisjs/core'
import type { Children } from '@praxisjs/shared'
@Component()
class MyButton extends BaseComponent {
@Slot() default?: Children
render() {
return <button>{this.default}</button>
}
}Props & State
@Prop()
Declares an external prop. The value comes from the parent; the decorated property acts as the default.
@Component()
class Card extends BaseComponent {
@Prop() title = 'Untitled'
@Prop() elevated = false
render() {
return <div class={this.elevated ? 'elevated' : ''}>{this.title}</div>
}
}@State()
Declares a reactive signal property. The getter returns the current value; the setter updates the underlying signal.
@Component()
class Toggle extends BaseComponent {
@State() open = false
render() {
return (
<button onClick={() => { this.open = !this.open }}>
{this.open ? 'Close' : 'Open'}
</button>
)
}
}Watching State
@Watch(...propNames)
Observes one or more @State, @Prop, or @Computed properties. The decorated method is called with the new and old values whenever they change.
import { Watch, WatchVal } from "@praxisjs/decorators";
@Component()
class Search extends BaseComponent {
@State() query = "";
@Watch("query")
onQueryChange(
newVal: WatchVal<this, "query">,
oldVal: WatchVal<this, "query">,
) {
console.log("query changed from", oldVal, "to", newVal);
}
}Watch multiple properties at once:
import { WatchVals } from '@praxisjs/decorators'
@Watch('firstName', 'lastName')
onNameChange(vals: WatchVals<this, 'firstName' | 'lastName'>) {
// vals is { firstName: newFirstName, lastName: newLastName }
}@When(propName)
Calls the decorated method exactly once, the first time the named property becomes truthy. Automatically starts on mount and cleans up on unmount.
@Component()
class Loader extends BaseComponent {
@State() data: string[] | null = null;
@When("data")
onFirstData() {
console.log("data arrived:", this.data);
}
}@History(limit?)
Adds undo/redo to a @State property. Accessible as {propName}History. Defaults to 50 entries.
@Component()
class Editor extends BaseComponent {
@History(100)
@State()
text = "";
undo() {
this.textHistory.undo();
}
redo() {
this.textHistory.redo();
}
}The {prop}History object exposes: undo(), redo(), canUndo, canRedo, values, clear().
Lifecycle Hooks
Lifecycle hooks can be used as class methods (via inheritance) or as standalone functions inside onMount / other hooks.
Functional hooks
import { onMount, onUnmount, onBeforeMount, onError } from '@praxisjs/core'
import { Component } from '@praxisjs/decorators'
import { BaseComponent } from '@praxisjs/core'
@Component()
class Timer extends BaseComponent {
render() {
onMount(() => {
const id = setInterval(() => console.log('tick'), 1000)
onUnmount(() => clearInterval(id))
})
return <div />
}
}| Hook | When it runs |
|---|---|
onBeforeMount(fn) | Before first render |
onMount(fn) | After first DOM insertion |
onUnmount(fn) | When component is removed from DOM |
onError(fn) | When an uncaught error occurs inside the component |
Class methods (via BaseComponent)
Override directly on the class:
@Component()
class MyComponent extends BaseComponent {
onMount() {
console.log('mounted')
}
onUnmount() {
console.log('unmounted')
}
render() { return <div /> }
}Events & Slots
@Emit(propName)
Binds the decorated method and calls the named prop callback with its return value. Ensures correct this binding.
@Component()
class Input extends BaseComponent {
@Prop() onChange?: (value: string) => void
@State() value = ''
@Emit('onChange')
handleInput(e: Event) {
this.value = (e.target as HTMLInputElement).value
return this.value // passed to onChange prop
}
render() {
return <input value={this.value} onInput={this.handleInput} />
}
}@OnCommand(propName)
Subscribes the decorated method to a Command prop. Automatically unsubscribes on unmount.
import { Command, createCommand } from '@praxisjs/decorators'
@Component()
class Modal extends BaseComponent {
@Prop() close?: Command
@OnCommand('close')
handleClose() {
console.log('modal closed by command')
}
render() { return <div /> }
}
// Usage:
const closeModal = createCommand()
<Modal close={closeModal} />
closeModal.trigger()@Slot(name?)
Declares a named slot. The getter returns the distributed children for that slot. Use without a name for the default slot.
INFO
@Slot() default captures all children that were not assigned to a named slot — equivalent to children in other frameworks.
@Component()
class Layout extends BaseComponent {
@Slot() default!: Children
@Slot('header') header!: Children
@Slot('footer') footer!: Children
render() {
return (
<div>
<header>{this.header}</header>
<main>{this.default}</main>
<footer>{this.footer}</footer>
</div>
)
}
}Command<T> and createCommand<T>()
An imperative event bus for triggering component actions from the outside.
import { createCommand } from "@praxisjs/decorators";
const reset = createCommand();
reset.trigger();
reset.subscribe(() => console.log("reset!"));Performance Decorators
@Memoize(areEqual?)
Class-level decorator that skips re-renders when the component's resolved props have not changed since the last render and no @State property was written. Equivalent to React.memo.
The optional areEqual(prev, next) function receives the previous and next resolved prop values and must return true when the component should not re-render. Defaults to a shallow (Object.is) equality check over all props.
import { Memoize, Component, Prop } from '@praxisjs/decorators'
import { BaseComponent } from '@praxisjs/core'
@Memoize()
@Component()
class Avatar extends BaseComponent {
@Prop() url = ''
@Prop() size = 48
render() {
return <img src={this.url} width={this.size} />
}
}If url and size haven't changed since the last render, the component skips its render entirely. Internal @State changes always trigger a re-render regardless.
Custom equality — deep comparison for object props:
function deepEqual(
prev: Record<string, unknown>,
next: Record<string, unknown>,
): boolean {
return JSON.stringify(prev) === JSON.stringify(next)
}
@Memoize(deepEqual)
@Component()
class Chart extends BaseComponent {
@Prop() data: DataPoint[] = []
render() { return <canvas /> }
}With deepEqual, passing a new array reference with the same contents does not trigger a re-render.
How it works:
- The decorator sets
_isMemorized = trueand stores_arePropsEqualon the constructor. - The renderer resolves any function-valued props (signals passed as props) on each re-render cycle, then compares the resolved values against the previous render's snapshot.
- A
_stateDirtyflag on the instance ensures that@Statewrites always bypass the memoize check.
@Lazy(placeholder?)
Defers rendering the component until it enters the viewport. Shows an empty placeholder element while off-screen.
@Lazy(300) // 300px placeholder height
@Component()
class HeavyChart extends BaseComponent {
render() { return <canvas /> }
}@Virtual(itemHeight, buffer?)
Virtualizes rendering for large lists. Only items in the visible viewport (plus buffer items on each side) are rendered.
@Virtual(48, 5)
@Component()
class UserList extends BaseComponent {
@Prop() items: User[] = []
renderItem(item: User, index: number) {
return <div key={item.id}>{item.name}</div>
}
render() { return <div /> }
}The items property and renderItem method are required.
Timing Decorators
@Debounce(ms)
Delays method execution by ms, canceling any previously scheduled call.
@Debounce(300)
onSearch(query: string) {
fetch(`/api/search?q=${query}`)
}@Throttle(ms)
Limits execution to at most once every ms milliseconds (leading edge).
@Throttle(1000)
onScroll(e: Event) {
this.scrollY = window.scrollY
}Utility Decorators
@Bind()
Automatically binds the method to the instance so it can be safely passed as a callback.
@Bind()
handleClick() {
console.log(this) // always the component instance
}
render() {
return <button onClick={this.handleClick}>Click</button>
}@Log(options?)
Logs method invocations with arguments, return value, and execution time. Dev-only by default.
@Log({ level: 'debug', time: true })
fetchData(id: number) {
return fetch(`/api/${id}`)
}| Option | Type | Default | Description |
|---|---|---|---|
level | 'log' | 'debug' | 'warn' | 'log' | Console method |
args | boolean | true | Log arguments |
result | boolean | true | Log return value |
time | boolean | false | Log execution time |
devOnly | boolean | true | Skip in production |
@Once()
Ensures the method runs at most once per instance. The result is cached and returned on subsequent calls.
@Once()
async loadConfig() {
const res = await fetch('/config.json')
return res.json()
}@Retry(maxAttempts, options?)
Automatically retries an async method on failure.
@Retry(3, { delay: 500, backoff: true })
async saveData(data: object) {
await api.save(data)
}| Option | Type | Description |
|---|---|---|
delay | number | Wait (ms) before first retry |
backoff | boolean | Double delay on each retry |
onRetry | (attempt, error) => void | Called before each retry |