Reactivity & Signals
PraxisJS uses signals as the core reactive primitive. A signal is a value that notifies its subscribers when it changes.
How it works
When render() runs, the renderer doesn't track dependencies. Only the arrow functions inside JSX create reactive subscriptions:
render() {
return (
<div>
<p>{() => this.count}</p> {/* ✅ reactive — updates when count changes */}
<p>{this.count}</p> {/* ❌ static — captured at render time */}
<p>{this.count * 2}</p> {/* ❌ static */}
<p>{() => this.count * 2}</p> {/* ✅ reactive */}
</div>
)
}Each arrow function {() => expr} becomes its own reactive effect — when any signal it reads changes, only that specific DOM node updates.
Signals via @State
@State() turns a class property into a reactive signal:
@Component()
class Timer extends StatefulComponent {
@State() seconds = 0
onMount() {
setInterval(() => this.seconds++, 1000)
}
render() {
return <p>Elapsed: {() => this.seconds}s</p>
}
}Reading this.seconds inside an arrow function subscribes to the signal. Writing this.seconds = x triggers all subscribers.
Computed values
@Computed() creates a cached, derived reactive value:
@Component()
class Cart extends StatefulComponent {
@State() items: { price: number }[] = []
@Computed()
get total() {
return this.items.reduce((sum, i) => sum + i.price, 0)
}
render() {
return <p>Total: {() => this.total}</p>
}
}total recalculates only when items changes — not on every read.
Plain getter vs @Computed
A plain get total() recalculates every time it's read, including inside reactive effects. @Computed() caches the result and only recomputes when its signal dependencies change.
Reactive arrays and objects
Signals track reference changes, not deep mutations:
// ✅ new reference — reactive
this.items = [...this.items, newItem]
this.config = { ...this.config, theme: 'dark' }
// ❌ in-place mutation — NOT reactive
this.items.push(newItem)
this.config.theme = 'dark'Always replace with a new reference when updating arrays or objects.
Passing props to child components
Props can be passed as static values or reactive getters — the difference determines whether the child updates when the parent's state changes.
Static prop — snapshot at render time
// ✅ valid — passes the current value of count at the moment Home renders
<Badge value={this.count} />The child receives 0 (or whatever the current value is). If count changes later, Badge is not notified — it keeps the original value. Use this when the child only needs the value once, or when the prop never changes.
Reactive prop — updates when the parent's state changes
// ✅ reactive — Badge re-evaluates value whenever count changes
<Badge value={() => this.count} />Passing a getter () => this.count lets the child pull the current value at any time. When count changes, any node inside Badge that reads this.value via {() => this.value} updates automatically.
Full example
interface BadgeProps {
value: number
}
@Component()
class Badge extends StatelessComponent<BadgeProps> {
render() {
// this.props.value is whatever the parent passed —
// if the parent passed () => this.count, the function itself lands here
// and the renderer handles it reactively automatically
return <span class="badge">{this.props.value}</span>
}
}
@Component()
class Counter extends StatefulComponent {
@State() count = 0
render() {
return (
<div>
{/* ✅ reactive — Badge updates when count changes */}
<Badge value={() => this.count} />
{/* ✅ static — Badge shows the value at render time, never updates */}
<Badge value={this.count} />
<button onClick={() => this.count++}>+</button>
</div>
)
}
}Rule of thumb
- Pass
() => this.statewhen the child should stay in sync with the parent. - Pass
this.statewhen you want to capture the value at the current moment and the child doesn't need to react to future changes.
Both forms are valid and safe — render() always runs untracked, so eager reads never cause unexpected side effects.
Props are reactive too
The pattern differs depending on whether the child uses @Prop() or raw this.props.
With @Prop() — the decorator unwraps the getter, so you need {() => this.text} to create the reactive binding:
@Component()
class Label extends StatefulComponent {
@Prop() text = ''
render() {
// @Prop() calls () => this.text internally and returns the value —
// wrap in () => so the renderer subscribes to it
return <span>{() => this.text}</span>
}
}With raw this.props (StatelessComponent) — this.props.text returns whatever the parent passed, including the function itself if the parent passed a getter. The renderer detects the function and makes it reactive automatically:
@Component()
class Label extends StatelessComponent<{ text: string }> {
render() {
// if parent passed text={() => this.name}, this.props.text IS the function —
// no extra () => needed
return <span>{this.props.text}</span>
}
}Reading without tracking
Sometimes you need to read a signal's current value without creating a reactive subscription — for example, inside a @Watch to compare a previous value, or inside a method that should not trigger re-subscriptions.
peek(signal)
Reads a signal once without subscribing to it:
import { peek } from '@praxisjs/core'
@Component()
class Counter extends StatefulComponent {
@State() count = 0
@State() max = 10
increment() {
// read max without subscribing to it inside a reactive context
if (peek(this.max) > peek(this.count)) {
this.count++
}
}
render() {
return <button onClick={() => this.increment()}>{() => this.count}</button>
}
}peek accepts any Signal<T> or Computed<T> and returns the current value without registering a dependency.
untrack(fn)
Runs an arbitrary function without tracking any signal reads inside it:
import { untrack } from '@praxisjs/core'
@Watch('items')
onItemsChange() {
// read totalCost without subscribing to it
const snapshot = untrack(() => this.totalCost)
console.log('items changed, cost was:', snapshot)
}When to use each
- Use
peek(signal)when you have a direct signal reference and want a clean, explicit read. - Use
untrack(fn)when you want to suppress tracking for a block of code that may read multiple signals.
What's next?
- JSX Syntax — reactive and static expressions in templates
- Decorators: State & Props —
@State,@Prop,@Computed,@Persisted