Utilities
Top-level helper functions exported by @domphy/core.
import { toState, merge, hashString } from "@domphy/core"Use Utilities here rather than Functions: these are reusable helper APIs, not the main object model like ElementNode, ElementList, or State.
toState(value, name?)
Creates a State from a raw value. If the input is already a State or ReadableState, returns it as-is.
const a = toState(0) // State<number>
const b = toState(a) // same State<number>, no wrapping
const c = toState(0, "count") // State<number> with debug name "count"| Parameter | Type | Description |
|---|---|---|
value | T | State<T> | ReadableState<T> | Raw value, existing State, or ReadableState |
name | string (optional) | Debug name for the state, used in devtools and error messages |
Returns State<T>.
Common use case: normalize patch props so callers can pass either a plain value or a reactive state.
const openState = toState(props.open ?? false)merge(source, target)
Deep-merges target into source using Domphy's composition rules.
const base = { class: "card", style: { padding: "1rem" } }
merge(base, { class: "active", style: { color: "red" } })
// base is now:
// { class: "card active", style: { padding: "1rem", color: "red" } }| Parameter | Type | Description |
|---|---|---|
source | Record<string, any> | Object to mutate |
target | Record<string, any> | Values to merge into source |
Returns the same source object after merge.
Key merge behaviors:
- Plain objects are merged deeply.
class,transform,reland similar fields are space-joined.animation,transition,boxShadowand similar fields are comma-joined.- Event handlers like
onClickare chained. - Hooks like
_onMountare chained. - Most other keys are overwritten by
target.
Use merge() when composing patches or mutating a raw element in _onSchedule.
behavior(key, attach, props)
Declares a per-node behavior (Svelte-action-like) inside a patch factory. attach(node, props) runs once for the real DOM node the returned partial lands on, no matter how many times the factory itself is re-invoked by a reactive parent — every later call routes its props into the SAME instance via update() instead of creating a new, disconnected one. destroy() fires exactly once when the node is removed.
import { behavior } from "@domphy/core"
function draggable(props: { onMove(dx: number, dy: number): void }) {
return behavior("draggable", (node, props) => {
const onPointerMove = (e: PointerEvent) => props.onMove(e.movementX, e.movementY)
node.domElement!.addEventListener("pointermove", onPointerMove)
return {
update: (next) => { props = next },
destroy: () => node.domElement!.removeEventListener("pointermove", onPointerMove),
}
}, props)
}| Parameter | Type | Description |
|---|---|---|
key | string | Identifies this concern on the element. Two DIFFERENT patches sharing one element (e.g. a tooltip and a popover on the same button) should use distinct keys so their instances stay independent. |
attach | (node: ElementNode, props: P) => { update?(props: P): void; destroy?(): void } | void | Runs once for the real node. Returns the instance's update/destroy callbacks. |
props | P | Passed to attach on first attach, and to update on every later re-declaration of the same key. |
Returns a PartialElement fragment ({ _behaviors: { [key]: { attach, props } } }) — compose it like any other patch field via object spread, merge(), or $.
Read a node's own attached instance with node.getBehavior(key) — it walks up through ancestors (like getContext/getMetadata), since the event that needs it often fires on a descendant of the element that declared the behavior (e.g. an inner <input>'s onFocus needing a behavior declared on its outer wrapper).
Use behavior() instead of a raw _onMount whenever a patch needs imperative state that must survive a reactive parent re-rendering the node it's attached to — document/window listeners, ResizeObserver/IntersectionObserver, non-Domphy library instances. See Reused-node lifecycle for why this matters, and Common Patterns → Per-node behavior for a fuller example.
hashString(str?)
Generates a deterministic string hash. The result always starts with a lowercase letter, so it is safe to use as a CSS identifier.
hashString("hello") // e.g. "b4a2f1c3"
hashString("hello") // same input, same output| Parameter | Type | Default | Description |
|---|---|---|---|
str | string | "" | Input string to hash |
Returns a string.
Primary use case: generate a stable animation name from keyframes.
const keyframes = { to: { transform: "rotate(360deg)" } }
const animationName = hashString(JSON.stringify(keyframes))
const style = {
animation: `${animationName} 0.7s linear infinite`,
[`@keyframes ${animationName}`]: keyframes,
}Do not use hashString() to generate ids for Domphy nodes. ElementNode already exposes node.nodeId, which is the runtime-scoped unique id used by the framework.
Notes:
- Deterministic: identical input always produces identical output.
- CSS-safe: output always starts with a letter.
- Not cryptographic: use it for IDs and CSS names, not security.
configure(options)
Set global runtime options. Call once before mounting your app.
import { configure } from "@domphy/core"
configure({ cspNonce: "abc123" })| Option | Type | Description |
|---|---|---|
cspNonce | string | Nonce stamped on every <style> element injected by Domphy. Required when your Content-Security-Policy uses style-src 'nonce-...' instead of 'unsafe-inline'. |
flushSync()
Synchronously drains all pending state-change notifications and the deduplicated effect/computed reaction queue. Useful in tests and imperative code that must observe the DOM immediately after .set() instead of waiting for the next microtask.
import { toState, flushSync } from "@domphy/core"
const count = toState(0)
count.set(1)
flushSync()
// count.get() === 1 and all downstream effects/computeds are settledIf a diverging reactive loop prevents settling, flushSync breaks after 10 000 iterations and logs a console.error. Inside batch(), batched writes still flush when the batch ends — flushSync does not flush them early.
runBatched(fn)
Runs fn inside a batch, coalescing all state writes into a single downstream flush. Equivalent to calling batch(fn) directly. Returns the value returned by fn.
import { runBatched } from "@domphy/core"
runBatched(() => {
a.set(10)
b.set(20)
})
// downstream effects/computeds re-run onceUse runBatched when passing a batch-wrapped callback to external code that expects a plain function signature.
hasPendingNotifiers()
Returns true if there are reactive notifications queued but not yet flushed.
import { hasPendingNotifiers } from "@domphy/core"
a.set(1)
hasPendingNotifiers() // true — flush has not run yetUseful in tests or scheduling code to check whether any state change is still pending before reading derived values.
flushPendingNotifiers()
Flushes all currently queued notifiers synchronously, without draining the full effect/computed reaction queue (unlike flushSync). Each pending notifier runs its downstream callbacks once.
import { flushPendingNotifiers } from "@domphy/core"
a.set(1)
flushPendingNotifiers()
// notifiers for `a` have fired; any newly queued notifiers are not flushedPrefer flushSync() when you need a fully settled reactive graph. Use flushPendingNotifiers() when you only need one notification pass (e.g. inside a scheduler that will call it in a loop).
computed(fn)
Creates a derived reactive value. fn is called without arguments — read states with .get() (no listener needed; dependencies are auto-tracked).
import { toState, computed } from "@domphy/core"
const count = toState(0)
const doubled = computed(() => count.get() * 2)
// doubled.get() === 0 initially; updates when count changesReturns Computed<T>. Read the value with .get(listener?) — the optional listener makes the call reactive in UI elements.
effect(fn)
Runs fn immediately and re-runs it whenever its tracked dependencies change.
import { toState, effect } from "@domphy/core"
const count = toState(0)
const stop = effect(() => {
console.log("count:", count.get())
})
// Logs on every count change. Call stop() to clean up.Returns a cleanup function () => void. Always call it in _onBeforeRemove or _onRemove to avoid memory leaks.
batch(fn)
Runs fn inside a batch, coalescing all state writes into a single downstream notification. Same as runBatched(fn).
import { toState, batch } from "@domphy/core"
const a = toState(0)
const b = toState(0)
batch(() => {
a.set(1)
b.set(2)
})
// effects/computeds depending on a or b re-run onceReturns the value returned by fn.
untrack(fn)
Runs fn without tracking any reactive reads. Dependencies accessed inside fn are not registered.
import { toState, effect, untrack } from "@domphy/core"
const source = toState(0)
const other = toState(0)
effect(() => {
const s = source.get() // tracked — effect re-runs when source changes
const o = untrack(() => other.get()) // NOT tracked — effect ignores other changes
console.log(s, o)
})watch(source, callback, options?)
Explicit watcher: runs callback whenever source changes.
import { toState, watch } from "@domphy/core"
const count = toState(0)
const stop = watch(count, (newVal, oldVal) => {
console.log("changed from", oldVal, "to", newVal)
})
// Call stop() to unsubscribesource can be a State or a getter function () => T. callback receives (newValue, oldValue). Options: { immediate?: boolean } — if true, runs callback immediately with the current value.
To watch multiple sources at once, compose them into a single getter — watch re-runs whenever any state read inside it changes:
const stop = watch(
() => [a.get(), b.get()] as const,
([newA, newB], prev) => {
console.log("a:", newA, "b:", newB)
},
)effectScope()
Creates a scope that collects all effect() calls made inside it. Calling scope.stop() cleans up all of them at once.
import { effectScope, effect, toState } from "@domphy/core"
const scope = effectScope()
const count = toState(0)
scope.run(() => {
effect(() => console.log("a:", count.get()))
effect(() => console.log("b:", count.get()))
})
scope.stop() // both effects cleaned upReturns EffectScopeHandle with .run(fn) and .stop() methods.
nextTick(fn?)
Defers fn to the next reactive flush microtask — after all pending state changes and effects settle.
import { toState, nextTick } from "@domphy/core"
const count = toState(0)
count.set(1)
await nextTick()
// all effects/computeds dependent on count have now re-run
// Or pass a callback:
nextTick(() => {
console.log("settled")
})Returns Promise<void>. Useful in tests and for reading DOM state after a reactive update.