PraxisJS

@praxisjs/decorators

Changelog for @praxisjs/decorators — @State, @Prop, @Computed, @Watch, @Emit, @Compose, and all built-in decorators.

@praxisjs/decorators

1.4.0

@Ref<T>() — DOM element ref decorator.

Replaces the manual { current: null as T | null } pattern with a typed decorator. The decorated field becomes a Ref<T> — callable as the JSX ref callback and holding the element in .current:

import { Ref } from '@praxisjs/decorators'

@Component()
class Card extends StatefulComponent {
  @Ref<HTMLDivElement>()
  cardRef!: Ref<HTMLDivElement>

  render() {
    return <div ref={this.cardRef}>...</div>
  }
}

Ref<T> is both the decorator function and the type annotation — one import, used in both positions.

Works with @Compose exactly as before — pass the field name as a string:

@Ref<HTMLDivElement>()
containerRef!: Ref<HTMLDivElement>

@Compose(ElementSize, 'containerRef')
size!: ElementSize

For refs outside of a class, use createRef<T>():

import { createRef } from '@praxisjs/decorators'

const scrollRoot = createRef<HTMLDivElement>()
// pass to @Lazy, store in module scope, etc.

1.3.0

@Computed — writable computed with getter + setter support.

@Computed now accepts get and set options for a TypeScript-compatible compact form using the accessor keyword — TypeScript sees the field as read-write, no cast required:

@Computed({
  get(this: MyComponent): number {
    return (this.celsius * 9) / 5 + 32
  },
  set(this: MyComponent, value: number) {
    this.celsius = ((value - 32) * 5) / 9
  },
})
accessor fahrenheit!: number

Alternatively, pass only set alongside a plain get accessor. This has identical runtime behavior but TypeScript cannot see the setter:

@Computed({
  set(this: MyComponent, value: number) {
    this.celsius = ((value - 32) * 5) / 9
  },
})
get fahrenheit(): number {
  return (this.celsius * 9) / 5 + 32
}

The getter is always a cached reactive computed() that recomputes when its signal dependencies change.

1.2.0

@When — optional condition function.

@When(propName, condition?) now accepts an optional predicate. The method fires exactly once, on the first value for which condition(value) returns true. Without a condition, the existing behaviour is preserved — fires on the first truthy value.

@When('score', score => score >= 100)
onWin() { ... }

Internal fix: @When now reads the property inside a reactive effect(), so it works correctly with @State-decorated fields in addition to raw Signal/Computed properties.

Internal test coverage raised to 100% across all decorators.

1.1.0

@Resource — cache, SWR, deduplication, invalidation, refetch on focus.

New options forwarded to the core resource() primitive:

  • key — shared cache key. Resources with the same key share a cache entry and a single in-flight request.
  • staleTime — ms before cached data is stale. At 0 (default) the cached value is shown immediately while fresh data loads in the background (SWR). Set to Infinity for a fully static cache.
  • refetchOnFocus — automatically refetches when the tab regains visibility.

New lifecycle behaviour:

  • The decorator now calls r.destroy() in onUnmount, ensuring focus listeners and cache registrations are cleaned up when the component is unmounted.

New export:

  • invalidateResource(key) — re-exported from @praxisjs/decorators for convenience. Clears the named cache entry and triggers all active resources under that key to refetch.

Internal: serializeArgs in @Memo — removed an unreachable null branch from the catch block (JSON.stringify(null) never throws).

import { invalidateResource } from '@praxisjs/decorators'

async function saveProfile(data: ProfileData) {
  await api.save(data)
  invalidateResource('current-user') // refetches every component subscribed to this key
}

1.0.2

@Resource now accepts a self-receiving fetcher — pass the component instance as the first argument to access reactive fields safely:

@Resource((self: MyComponent) => api.getPage(self.page))
items!: ResourceInstance<Item[]>

When the fetcher accepts one argument, the component instance is passed automatically at bind time. Any signals read on self become reactive dependencies. The zero-argument form (() => api.getAll()) continues to work unchanged.

1.0.1

Updated dependencies — @praxisjs/[email protected].

1.0.0

Breaking — @Virtual removed

@Virtual is removed. It had two fundamental flaws: items were a snapshot at render time and never updated reactively, and JSX in renderItem silently failed on scroll.

Migrate to the VirtualList composable — it exposes reactive signals (visibleItems, totalHeight, offsetTop, offsetBottom) and renders with normal JSX.

Other changes in this release:

  • @Lazy fixed — the component now renders after entering the viewport. The previous implementation returned a static null from the render enhancement so the IntersectionObserver callback had no effect
  • @Lazy options object — now accepts { placeholder, root, rootMargin } for scoping intersection to a specific scroll container
  • @Compose string literals@Compose(KeyCombo, 'ctrl+s') no longer requires an intermediate instance property. New getter(propName) helper exported for live reactive sources
  • Bug fix — stacked class decorators (e.g. @Lazy @Component) no longer cause infinite recursion

0.8.1

@State and @DeepState now produce a TypeScript error when applied to plain classes. Store classes must extend ReactiveStore from @praxisjs/store.

0.8.0

  • New: @Synced(channelName?) — syncs a field across browser tabs via BroadcastChannel
  • New: @DeepState() — deep Proxy so nested mutations are reactive without replacing the reference

0.7.0

Breaking — @History redesigned

@History now decorates a separate field (not the @State field itself):

// before
@History(100) @State() text = ''

// after
@State() text = ''
@History('text', 100) textHistory!: HistoryOf<MyClass, 'text'>

0.6.0

  • New: @Until(propName) — replaces the method with one that returns a Promise resolving to the first truthy value
  • @Memo falls back to object identity for non-JSON-serializable arguments
  • @Debounce cancels its timer on component unmount
  • @Throttle clamps negative ms to 0

0.5.0

  • Decorator factory helperscreateFieldDecorator, createClassDecorator, createMethodDecorator, createLifecycleMethodDecorator, createGetterDecorator, createGetterObserverDecorator. See Creating Decorators
  • New: @Compose — mixes a Composable class into a component
  • New: @Resource — async data field on a component

0.4.3

Bug fixes in @Lazy, @Virtual, and @Watch: type constraint incompatibility, infinite recursion on render after becoming visible, reactive effect leak on unmount.

0.4.0

Added @Computed() for read-only cached reactive getters. @Debug() now supports @Computed getters.

0.3.0

Migrated to TC39 decorator context API. Introduced StatefulComponent / StatelessComponent.

0.1.0

Initial beta release.

On this page