Creating Decorators
How to build custom decorators in PraxisJS using createFieldDecorator, createMethodDecorator, createLifecycleMethodDecorator, createGetterDecorator, createGetterObserverDecorator, and createClassDecorator.
Creating Decorators
PraxisJS provides factory functions for creating decorators. Each handles a different use case and manages per-instance state correctly.
Internal APIs
Decorator implementations have access to the reactive engine through @praxisjs/core/internal — signal, computed, effect, batch, peek, and more. See the Internal APIs reference for the full list.
Field decorators
Use createFieldDecorator to replace a property with custom reactive behavior.
bind() is called once per instance when the class initializes. Return a descriptor to replace the property with a custom getter/setter.
Example — @SessionValue stores a value in sessionStorage and stays reactive:
import { createFieldDecorator } from '@praxisjs/decorators'
import { signal } from '@praxisjs/core/internal'
export function SessionValue(key: string) {
return createFieldDecorator({
bind(_instance, name) {
const stored = sessionStorage.getItem(key)
const _value = signal(stored ?? '')
return {
descriptor: {
get() { return _value() },
set(v: string) {
_value.set(v)
sessionStorage.setItem(key, v)
},
},
}
},
})
}@Component()
class SearchPage extends StatefulComponent {
@SessionValue('search:query')
query = ''
render() {
return (
<input
value={() => this.query}
onInput={(e) => { this.query = (e.target as HTMLInputElement).value }}
/>
)
}
}bind() receives (instance, name, initialValue). The FieldBinding you return can also include onMount, onUnmount, and additional (extra properties to define on the instance).
Method decorators
Use createMethodDecorator to wrap a method with cross-cutting behavior.
wrap() receives the original function and returns a replacement. It's called once per instance.
Example — @Confirm shows a dialog before running:
import { createMethodDecorator } from '@praxisjs/decorators'
export function Confirm(message: string) {
return createMethodDecorator({
wrap(original) {
return function (this: object, ...args: unknown[]) {
if (window.confirm(message)) {
return original.apply(this, args)
}
}
},
})
}@Confirm('Delete this item permanently?')
async deleteItem(id: number) {
await api.delete(id)
}For per-instance state, use a WeakMap keyed on the instance:
export function CountCalls() {
const counts = new WeakMap<object, number>()
return createMethodDecorator({
wrap(original, instance) {
counts.set(instance, 0)
return function (this: object, ...args: unknown[]) {
counts.set(this, (counts.get(this) ?? 0) + 1)
return original.apply(this, args)
}
},
})
}Lifecycle method decorators
Use createLifecycleMethodDecorator to automatically register a method as a listener on mount and clean it up on unmount — without the component knowing about it.
register(callback, instance) is called inside onMount. If it returns a function, that function is called on onUnmount as cleanup.
Example — @OnResize calls a method whenever the window is resized:
import { createLifecycleMethodDecorator } from '@praxisjs/decorators'
export function OnResize() {
return createLifecycleMethodDecorator({
register(callback) {
window.addEventListener('resize', callback)
return () => window.removeEventListener('resize', callback)
},
})
}@Component()
class Layout extends StatefulComponent {
@State() cols = 3
@OnResize()
recalculate() {
this.cols = window.innerWidth > 1024 ? 4 : window.innerWidth > 640 ? 3 : 1
}
render() {
return <Grid cols={() => this.cols} />
}
}No manual addEventListener/removeEventListener needed — the decorator handles it.
Getter decorators
createGetterDecorator — transform or constrain a getter's value
wrap(original, instance) is called on every property access and should return a function that computes the final value.
Example — @Clamp(min, max) constrains a getter to a numeric range:
import { createGetterDecorator } from '@praxisjs/decorators'
export function Clamp(min: number, max: number) {
return createGetterDecorator({
wrap(original, instance) {
return () => {
const value = original.call(instance) as number
return Math.min(max, Math.max(min, value))
}
},
})
}@Component()
class Slider extends StatefulComponent {
@State() raw = 0
@Clamp(0, 100)
get value() { return this.raw }
render() {
return <p>{() => this.value}</p> // always between 0 and 100
}
}createGetterObserverDecorator — observe a getter for side effects
Observes a getter without changing its return value. observe(getter, instance, name) is called once per instance at initialization — set up a reactive effect here.
Example — @LogChange logs whenever a computed getter produces a new value:
import { createGetterObserverDecorator } from '@praxisjs/decorators'
import { effect } from '@praxisjs/core/internal'
export function LogChange() {
return createGetterObserverDecorator({
observe(getter, instance, name) {
effect(() => {
console.log(`[${name}]`, getter.call(instance))
})
},
})
}@LogChange()
@Computed()
get filteredItems() {
return this.items.filter(i => i.active)
}The observer runs once on setup, then again whenever a signal read inside getter changes.
Class decorators
Use createClassDecorator to augment the component lifecycle or wrap render().
Extend ClassBehavior and implement create(), called once per instance. Return a ClassEnhancement with onMount, onUnmount, and/or render.
Example — @Analytics tracks mount/unmount events automatically:
import { createClassDecorator, ClassBehavior } from '@praxisjs/decorators'
class AnalyticsBehavior extends ClassBehavior {
constructor(private readonly name: string) { super() }
create() {
const name = this.name
return {
onMount() { analytics.track('mount', { component: name }) },
onUnmount() { analytics.track('unmount', { component: name }) },
}
}
}
export function Analytics(name: string) {
return createClassDecorator(new AnalyticsBehavior(name))
}@Analytics('UserDashboard')
@Component()
class UserDashboard extends StatefulComponent { /* ... */ }To wrap rendering, return a render function that receives originalRender and can modify or wrap its output:
create(_instance) {
return {
render(originalRender) {
const nodes = originalRender()
// wrap with an error boundary, portal, loading gate, etc.
return nodes
},
}
}Internal API reference
Decorator implementations commonly use these internal primitives:
| Import | From | Description |
|---|---|---|
signal | @praxisjs/core/internal | Create reactive state inside a decorator binding. |
computed | @praxisjs/core/internal | Derive a memoized value from signals. |
effect | @praxisjs/core/internal | Run side effects that re-run when signals change. |
batch | @praxisjs/core/internal | Group multiple signal writes into a single notification. |
peek | @praxisjs/core/internal | Read a signal without subscribing. |
RootComponent | @praxisjs/core/internal | Type the instance parameter in class decorators. |
isSignal | @praxisjs/shared/internal | Check if a value is a signal at runtime. |
ComponentConstructor | @praxisjs/shared/internal | Type a component class accepted as a parameter. |
Full reference → @praxisjs/core/internal · @praxisjs/shared/internal