Skip to content

State & Props

@State()

Declares a reactive property backed by a signal. Reading it inside an arrow function in JSX subscribes to updates.

tsx
@Component()
class Toggle extends StatefulComponent {
  @State() open = false

  render() {
    return (
      <button onClick={() => { this.open = !this.open }}>
        {() => this.open ? 'Close' : 'Open'}
      </button>
    )
  }
}

Arrays and objects

@State tracks reference changes only. Mutate with new references:

ts
// ✅
this.items = [...this.items, newItem]
this.config = { ...this.config, theme: 'dark' }

// ❌ won't trigger updates
this.items.push(newItem)
this.config.theme = 'dark'

@Prop()

Declares an external prop. The initialized value is the default when the parent doesn't pass one.

tsx
@Component()
class Button extends StatefulComponent {
  @Prop() label = 'Click me'
  @Prop() disabled = false

  render() {
    return (
      <button disabled={() => this.disabled}>
        {() => this.label}
      </button>
    )
  }
}

// Usage:
<Button label="Submit" disabled={false} />

@Computed()

Declares a read-only derived getter backed by a cached reactive computation. Recomputes only when its signal dependencies change.

tsx
@Component()
class Cart extends StatefulComponent {
  @State() items: { name: string; price: number }[] = []

  @Computed()
  get total() {
    return this.items.reduce((sum, i) => sum + i.price, 0)
  }

  render() {
    return <p>Total: ${() => this.total}</p>
  }
}

@Persisted(key?, options?)

Like @State, but the value is persisted to localStorage and survives page reloads. key defaults to the property name.

tsx
@Component()
class Settings extends StatefulComponent {
  @Persisted() theme = 'light'
  @Persisted('app:fontSize') fontSize = 14

  render() {
    return (
      <select value={() => this.theme} onChange={(e) => {
        this.theme = (e.target as HTMLSelectElement).value
      }}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
    )
  }
}

Setting the value to null or undefined removes the entry from localStorage.

OptionTypeDefaultDescription
serialize(v: T) => stringJSON.stringifyCustom serializer
deserialize(s: string) => TJSON.parseCustom deserializer
syncTabsbooleantrueSync across browser tabs

@History(fieldName, limit?)

Adds undo/redo to a @State property. Declare a separate field decorated with @History('fieldName') and type it with HistoryOf<Class, 'fieldName'> for full intellisense.

tsx
import { History, HistoryOf } from '@praxisjs/decorators'

@Component()
class Editor extends StatefulComponent {
  @State()
  text = ''

  @History('text', 100)
  textHistory!: HistoryOf<Editor, 'text'>
  // textHistory.undo()    ✓
  // textHistory.redo()    ✓
  // textHistory.canUndo() ✓
  // textHistory.canRedo() ✓

  render() {
    return (
      <div>
        <textarea value={() => this.text}
          onInput={(e) => { this.text = (e.target as HTMLTextAreaElement).value }} />
        <button onClick={() => this.textHistory.undo()} disabled={() => !this.textHistory.canUndo()}>Undo</button>
        <button onClick={() => this.textHistory.redo()} disabled={() => !this.textHistory.canRedo()}>Redo</button>
      </div>
    )
  }
}

@Resource(fetcher, options?)

Binds an async resource to the field. Automatically re-fetches when any signal read inside the fetcher changes.

tsx
@Component()
class PostList extends StatefulComponent {
  @State() page = 1

  @Resource(() => fetch(`/api/posts?page=${this.page}`).then(r => r.json()))
  posts!: ResourceInstance<Post[]>

  render() {
    return (
      <div>
        {() => this.posts.pending() && <Spinner />}
        {() => this.posts.data()?.map(p => <PostCard post={p} />)}
        <button onClick={() => this.page++}>Next page</button>
      </div>
    )
  }
}
OptionDefaultDescription
immediatetrueFetch on initialization
initialDatanullValue of .data() before first fetch
keepPreviousDatafalseKeep old data while refetching

The field exposes .data(), .pending(), .error(), .status(), .refetch(), .cancel(), and .mutate(value).

→ See Async Data for full details.

Released under the MIT License.