Skip to content
Domphy

State

Reactive value container. When the value changes, all listeners are notified.

ReadableState<T>

A read-only view of a State<T>. Exposes only get(listener?) — no set, reset, or addListener. Use it when you want to pass a state to a consumer that should read but not mutate it.

export type ReadableState<T> = {
  readonly _isState: true;
  get(listener?: ValueListener<T>): T;
};

The _isState: true discriminant lets runtime code and type guards distinguish a ReadableState from a plain value.

import type { ReadableState } from "@domphy/core"

function display(count: ReadableState<number>) {
  return { p: (l) => `Count: ${count.get(l)}` }
}

ReadableState<T> is exported as a named type from @domphy/core. State<T> satisfies ReadableState<T> — any State can be passed where a ReadableState is expected. toState() also accepts ReadableState<T> as input (returns it as-is).

import { toState } from "@domphy/core"

const count = toState(0)

count.get()        // 0
count.set(1)       // notify all listeners
count.get()        // 1
count.reset()      // back to 0

Create a State with toState() from @domphy/core. The toState() function is documented in the Utilities page.

Methods

get(listener?)

Returns the current value. If a listener is provided, subscribes it to future changes.

const value = count.get()

// With listener — auto-subscribe
const value = count.get(listener)

set(newValue)

Updates the value and notifies all listeners.

count.set(5)

reset()

Resets the value to initialValue.

const filter = toState("all")
filter.set("active")
filter.reset()
filter.get()  // "all"

addListener(listener)

Subscribes a listener to value changes. Returns a release function.

const release = count.addListener((value) => {
  console.log(value)
})

// Unsubscribe
release()

Reactive children

Pass a function as children to make an element reactive:

const count = toState(0)

const node: DomphyElement = {
  p: (listener) => `Count: ${count.get(listener)}`
//                                      ↑ subscribes automatically
}

When count.set() is called, the element re-renders automatically.

initialValue

The value passed to the constructor. Used by reset().

const count = toState(0)
count.initialValue  // 0

ValueOrState<T>

A union type accepted by patch props and element attributes that can be either a plain value, a reactive State<T>, or a read-only ReadableState<T>.

export type ValueOrState<T> = T | State<T> | ReadableState<T>;

Use it in function signatures when a prop should accept both static values and reactive states:

import type { ValueOrState } from "@domphy/core"

function myPatch(open: ValueOrState<boolean>): PartialElement {
  return {
    ariaExpanded: typeof open === "object" && open._isState
      ? (l) => (open as ReadableState<boolean>).get(l)
      : open,
  }
}

In practice most patch props accept ValueOrState<T> so callers can pass true / false or a toState(false) interchangeably.


RecordState<T>

Per-key reactive record. Unlike State<Record<...>> which notifies all listeners on every change, RecordState notifies only listeners for the specific key that changed.

import { RecordState } from "@domphy/core"

const form = new RecordState({ name: "", age: 0 })

Methods

get(key, listener?)

Read a field value. Registers listener (or the active computed/effect collector) for the given key only.

const name = form.get("name")              // untracked read
const nameEl = { span: (l) => form.get("name", l) }  // reactive — updates when name changes

set(key, value)

Write a field. Notifies only listeners subscribed to that key.

form.set("name", "Alice")
form.set("age", 30)

addListener(key, fn) / removeListener(key, fn)

Manually subscribe/unsubscribe a callback for a specific key. addListener returns an unsubscribe function.

reset(key)

Restores a field to its initialRecord value.

form.reset("name")   // back to ""

initialRecord

The original record passed to the constructor. Read-only reference, used by reset().

_dispose()

Removes all listeners from all notifiers. Call in _onRemove if you create a RecordState inside a component.

When to use

Prefer RecordState over a single State<Record<...>> when the record has multiple frequently-changing fields and different UI elements subscribe to different fields. Each setter triggers only the relevant listeners instead of all of them.