Utility Decorators
General-purpose method decorators — @Bind, @Log, @Once, @Memo, and @Retry.
Utility Decorators
General-purpose decorators for cross-cutting concerns: binding, logging, caching, and retry logic.
npm install @praxisjs/decoratorspnpm add @praxisjs/decoratorsyarn add @praxisjs/decoratorsbun add @praxisjs/decorators@Bind()
Binds the method to the instance. Useful when you need to pass a method as a callback without wrapping it in an arrow function every time.
@Component()
class Panel extends StatefulComponent {
@State() open = true
@Bind()
handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') this.open = false
}
onMount() {
// `this` is correct because of @Bind — no arrow wrapper needed
window.addEventListener('keydown', this.handleKeyDown)
}
onUnmount() {
window.removeEventListener('keydown', this.handleKeyDown)
}
render() {
return () => this.open && <div class="panel">...</div>
}
}Every read returns the same bound function, so removeEventListener receives the listener that was added. The bound copy is created the first time the method is read from an instance, so instances that never read it pay nothing.
@Once, @Memo, @Log, @Retry, @Debounce, @Throttle and @Emit return bound methods the same way, so they can be passed as callbacks directly too.
@Log(options?)
Logs method calls with arguments, return value, and execution time. Dev-only by default — stripped from production builds when devOnly: true.
@Log({ level: 'debug', time: true })
async fetchUser(id: number) {
return fetch(`/api/users/${id}`).then(r => r.json())
}
// Logs: fetchUser(42) → { id: 42, name: 'Jane' } [12ms]| Option | Type | Default | Description |
|---|---|---|---|
level | 'log' | 'debug' | 'warn' | 'log' | Console method to use |
args | boolean | true | Include arguments in the log |
result | boolean | true | Include the return value |
time | boolean | false | Include execution time in ms |
devOnly | boolean | true | Skip in production environments |
@Once()
Ensures the method runs at most once per instance. The return value is cached and returned on all subsequent calls — the original function never runs again.
@Component()
class AppConfig extends StatefulComponent {
@Once()
async loadConfig() {
const res = await fetch('/config.json')
return res.json()
// Fetched once — every subsequent call returns the cached result
}
onMount() {
this.loadConfig().then(config => console.log(config))
this.loadConfig().then(config => console.log(config)) // same result, no second request
}
}@Memo()
Memoizes the return value per unique argument combination. Each unique set of arguments gets its own reactive computed() — so if the method reads any @State or @Prop, the cache updates automatically when those signals change.
@Component()
class PriceList extends StatefulComponent {
@State() discount = 0
@Memo()
discountedPrice(price: number) {
return price * (1 - this.discount)
}
render() {
return (
<ul>
<li>$100 → {() => this.discountedPrice(100)}</li>
<li>$200 → {() => this.discountedPrice(200)}</li>
<li>$500 → {() => this.discountedPrice(500)}</li>
</ul>
)
}
}When this.discount changes, all three cached values recompute. Each argument combination has an independent cache entry.
The cache keeps the 100 most recently used argument combinations; the least recently used entry is dropped once the limit is exceeded, and its subscriptions to the signals it read are released (unless a binding is still using it). Pass max to change the limit, or Infinity to keep every combination for the life of the component:
@Memo({ max: 50 })
search(query: string) {
return this.items.filter((i) => i.name.includes(query))
}How argument caching works
Arguments are serialized as a string key:
- Objects/null →
JSON.stringify(falls back to object identity for non-serializable values) - Symbols →
symbol.toString() - Everything else →
String(value)
@Retry(maxAttempts, options?)
Automatically retries an async method on failure. The method is called again after delay ms, with the delay multiplied by backoff on each subsequent attempt.
@Retry(3, { delay: 500, backoff: 2 })
async saveData(data: object) {
const res = await fetch('/api/save', {
method: 'POST',
body: JSON.stringify(data),
})
if (!res.ok) throw new Error(`Save failed: ${res.status}`)
}
// Attempts at: 0ms, 500ms, 1000ms — then throws if all fail| Option | Type | Description |
|---|---|---|
delay | number | Wait (ms) before the first retry |
backoff | number | Multiply delay by this factor on each retry (2 doubles it) |
onRetry | (error, attempt) => void | Called before each retry attempt |