Creating Composables
How to build custom composable classes in PraxisJS by extending Composable and using them with the @Compose decorator.
Creating Composables
A composable encapsulates reactive state and external behavior into a reusable class. Use @Compose to attach it to any component — the reactive properties and lifecycle hooks wire up automatically.
npm install @praxisjs/core @praxisjs/decoratorspnpm add @praxisjs/core @praxisjs/decoratorsyarn add @praxisjs/core @praxisjs/decoratorsbun add @praxisjs/core @praxisjs/decoratorsInternal APIs
Composable implementations use signal, computed, effect, and other primitives from @praxisjs/core/internal. See the Internal APIs reference for the full list including history, debounced, resource, and persistedSignal.
Basic structure
Every composable extends Composable and implements setup(). The setup() method creates signals and returns them. Properties declared with declare provide TypeScript types — the actual reactive getters are wired up at runtime.
import { signal } from '@praxisjs/core/internal'
import { Composable } from '@praxisjs/core'
export class NetworkStatus extends Composable {
declare online: boolean // typed property — reactive getter wired at runtime
setup() {
const online = signal(navigator.onLine)
const onOnline = () => online.set(true)
const onOffline = () => online.set(false)
window.addEventListener('online', onOnline)
window.addEventListener('offline', onOffline)
this._cleanup = () => {
window.removeEventListener('online', onOnline)
window.removeEventListener('offline', onOffline)
}
return { online } // keys must match the declared properties
}
private _cleanup = () => {}
onUnmount() {
this._cleanup()
}
}Use it in any component:
import { Compose } from '@praxisjs/decorators'
import { NetworkStatus } from './composables/network-status'
@Component()
class App extends StatefulComponent {
@Compose(NetworkStatus)
network!: NetworkStatus
render() {
return (
<div>
{() => this.network.online
? <p>Connected</p>
: <p>No internet connection</p>
}
</div>
)
}
}What the field holds
The decorated field holds a view of what setup() returned, built once per component instance: signals and computeds become getters (so this.network.online reads the signal and tracks it inside arrow functions), and everything else, such as functions, is copied as is. A method of the composable class is reachable only if setup() returns it.
onMount() and onUnmount() on the composable run after the component's own hooks. Only the hooks the class defines are chained, so a composable without them adds no lifecycle work to the component.
Constructor arguments
Add a constructor to accept configuration. Extra arguments to @Compose after the class are passed to the constructor:
export class PollingResource<T> extends Composable {
declare data: T | null
declare loading: boolean
constructor(
private readonly url: string,
private readonly interval = 5000,
) {
super()
}
setup() {
const data = signal<T | null>(null)
const loading = signal(true)
const fetchData = () => {
window.fetch(this.url)
.then(r => r.json() as T)
.then(v => { data.set(v); loading.set(false) })
}
fetchData()
const timer = setInterval(fetchData, this.interval)
this._stop = () => clearInterval(timer)
return { data, loading }
}
private _stop = () => {}
onUnmount() { this._stop() }
}@Compose(PollingResource, '/api/status', 10_000)
status!: PollingResource<StatusData>String arguments — ref resolution
String arguments to @Compose resolve to instance property values at bind time. This is useful for passing element refs:
containerRef = { current: null as HTMLElement | null }
@Compose(ElementSize, 'containerRef')
size!: ElementSize
// The composable receives this.containerRef — the ref object, not a stringgetter(propName) — live reactive source
When the composable needs a callable source (a Signal<T> or () => T) rather than a snapshot, use getter() to wrap a property name as a live getter:
import { getter } from '@praxisjs/decorators'
@State() postedAt = new Date()
// ✅ passes () => this.postedAt — TimeAgo calls it reactively
@Compose(TimeAgo, getter('postedAt'))
timeAgo!: TimeAgo
// ❌ passes the current Date value — TimeAgo can't call a Date
@Compose(TimeAgo, 'postedAt')getter('propName') resolves to () => instance[propName] at bind time. If the property is a @State signal or a computed getter, reads inside reactive effects track it correctly.
| Form | What the composable receives | When to use |
|---|---|---|
'propName' | Current value of instance.propName | Refs, plain objects, config |
getter('propName') | () => instance.propName (callable) | Signals, computed getters, anything that must stay reactive |
Derived state with computed
setup() can return signals, computeds, and plain functions — all are exposed as properties:
import { signal, computed } from '@praxisjs/core/internal'
export class Counter extends Composable {
declare count: number
declare doubled: number
declare increment: () => void
declare reset: () => void
setup() {
const count = signal(0)
const doubled = computed(() => count() * 2)
return {
count,
doubled,
increment: () => count.update(n => n + 1),
reset: () => count.set(0),
}
}
}@Compose(Counter)
counter!: Counter
render() {
return (
<div>
<p>{() => this.counter.count} (×2 = {() => this.counter.doubled})</p>
<button onClick={() => this.counter.increment()}>+1</button>
<button onClick={() => this.counter.reset()}>Reset</button>
</div>
)
}Internal API reference
Composable implementations commonly use these internal primitives:
| Import | From | Description |
|---|---|---|
signal | @praxisjs/core/internal | Reactive state returned from setup(). |
computed | @praxisjs/core/internal | Derived values exposed as composable properties. |
effect | @praxisjs/core/internal | Side effects that track signal dependencies. |
resource | @praxisjs/core/internal | Reactive async data fetching with status tracking. |
debounced | @praxisjs/core/internal | A signal that delays updates by a given time. |
history | @praxisjs/core/internal | Undo/redo history tracking for a signal. |
persistedSignal | @praxisjs/core/internal | Signal backed by localStorage. |
isReactive | @praxisjs/shared/internal | Check if a constructor argument is a signal or computed. |
Full reference → @praxisjs/core/internal · @praxisjs/shared/internal
Extending PraxisJS
Build custom composables and decorators using the same primitives that power all built-in PraxisJS APIs.
Creating Decorators
Build your own decorators with createFieldDecorator, createMethodDecorator, createLifecycleMethodDecorator, createGetterDecorator, createWritableGetterDecorator, createGetterObserverDecorator and createClassDecorator. Covers the shared setup form, typing, performance, testing and migrating from createAccessorDecorator.