PraxisJS

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.


@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>
  }
}
Storybook
Live demo — @Bind

@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]
OptionTypeDefaultDescription
level'log' | 'debug' | 'warn''log'Console method to use
argsbooleantrueInclude arguments in the log
resultbooleantrueInclude the return value
timebooleanfalseInclude execution time in ms
devOnlybooleantrueSkip 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
  }
}
Storybook
Live demo — @Once

@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.

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)
Storybook
Live demo — @Memo

@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
OptionTypeDescription
delaynumberWait (ms) before the first retry
backoffnumberMultiply delay by this factor on each retry (2 doubles it)
onRetry(error, attempt) => voidCalled before each retry attempt
Storybook
Live demo — @Retry

On this page