PraxisJS

Watchers

React to state changes with @Watch, @When, and @Until — declarative side-effect handlers and async waiters for reactive properties.

Watchers

Watcher decorators let you react to state changes declaratively, without setting up effects manually in onMount.

npm install @praxisjs/decorators
pnpm add @praxisjs/decorators
yarn add @praxisjs/decorators
bun add @praxisjs/decorators

@Watch(...propNames)

Calls the decorated method whenever the watched property changes. The method receives the new value and old value as arguments.

import { Component, State, Watch } from '@praxisjs/decorators'
import { WatchVal } from '@praxisjs/decorators'

@Component()
class Search extends StatefulComponent {
  @State() query = ''

  @Watch('query')
  onQueryChange(newVal: WatchVal<this, 'query'>, oldVal: WatchVal<this, 'query'>) {
    console.log(`query: "${oldVal}" → "${newVal}"`)
    this.fetchResults(newVal)
  }

  async fetchResults(q: string) { /* ... */ }

  render() {
    return (
      <input
        value={() => this.query}
        onInput={(e) => { this.query = (e.target as HTMLInputElement).value }}
      />
    )
  }
}

Watch multiple properties

When watching multiple props, the method receives an object with all current values:

import { WatchVals } from '@praxisjs/decorators'

@Component()
class Form extends StatefulComponent {
  @State() firstName = ''
  @State() lastName = ''

  @Watch('firstName', 'lastName')
  onNameChange(vals: WatchVals<this, 'firstName' | 'lastName'>) {
    console.log(`Name: ${vals.firstName} ${vals.lastName}`)
  }
}

Coalesced updates

When multiple watched props change in the same synchronous block, the callback fires once with all final values — not once per changed prop. Changes made to signals inside the callback are also batched automatically.

this.firstName = 'Jane'
this.lastName = 'Smith'
// → onNameChange fires once with { firstName: 'Jane', lastName: 'Smith' }
Storybook
Live demo — @Watch single prop
Storybook
Live demo — @Watch multi-prop coalescing

@When(propName, condition?)

Calls the decorated method exactly once, the first time the named property satisfies the condition. Automatically set up on mount and cleaned up on unmount.

@Component()
class DataLoader extends StatefulComponent {
  @State() data: string[] | null = null

  @When('data')
  onFirstLoad() {
    console.log('Data arrived for the first time:', this.data)
    // safe to use this.data here — it's truthy
  }

  render() {
    return () => this.data
      ? <ul>{() => this.data!.map(d => <li>{d}</li>)}</ul>
      : <p>Loading...</p>
  }
}

Use @When for one-time initialization that depends on a value arriving — like the first API response, or a user becoming authenticated.

Storybook
Live demo — @When

Condition function

Pass an optional predicate as the second argument. The method fires the first time the predicate returns true for the current value. Without a condition, the method fires on the first truthy value.

@Component()
class Game extends StatefulComponent {
  @State() score = 0

  // fires when score reaches 100 for the first time
  @When('score', score => score >= 100)
  onWin() {
    console.log('You won with score:', this.score)
  }

  // fires the first time the user's plan is 'pro' or 'enterprise'
  @When('plan', plan => plan === 'pro' || plan === 'enterprise')
  onUpgrade(plan: string) {
    this.enablePremiumFeatures(plan)
  }
}

The predicate receives the current value of the property. The method is called only once — on the first value that satisfies the condition. Subsequent changes that also satisfy the condition do not re-fire it.

Storybook
Live demo — @When with condition

@Until(propName)

Replaces the decorated method with one that returns a Promise resolving to the first truthy value of the named property. Each call to the method returns a fresh promise.

import { Component, State, Until } from '@praxisjs/decorators'

@Component()
class UserProfile extends StatefulComponent {
  @State() user: User | null = null

  @Until('user')
  waitForUser(): Promise<User> { return Promise.resolve(null!) }
  // The method body is replaced entirely — it's just a type hint.

  async loadProfile() {
    const user = await this.waitForUser()
    console.log('User is ready:', user.name)
  }

  render() { /* ... */ }
}

If the property is already truthy when the method is called, the promise resolves on the next microtask.

Use @Until when downstream code needs to await a reactive value rather than react to it with a side effect. If you're showing/hiding UI based on the value, use @When or @Watch instead.

Storybook
Live demo — @Until

On this page