Reactivity
Model state with refs, derive values with computed, and coordinate updates.
This page describes Regor reactivity based on actual runtime behavior.
Keep writable state small. Derive the rest with computed values.
Try it live
- Subtotal
- $100.00
- Discount
- $0.00
Two inputs. Three computed values. One connected view.
import { batch, computed, html, ref } from 'regor'
export function createQuote(interactive = false) {
const quantity = ref(4)
const unitPrice = ref(25)
const discounted = ref(false)
const subtotal = computed(() => quantity() * unitPrice())
const savings = computed(() => (discounted() ? subtotal() * 0.1 : 0))
const total = computed(() => subtotal() - savings())
const money = (value: number) => `$${value.toFixed(2)}`
const reset = () =>
batch(() => {
quantity(4)
unitPrice(25)
discounted(false)
})
return {
interactive,
quantity,
unitPrice,
discounted,
subtotal,
savings,
total,
money,
reset,
}
}
export const quoteTemplate = html` <div class="guide-demo guide-demo--split">
<div class="guide-controls">
<label for="quote-quantity">Seats <strong>{{ quantity }}</strong></label>
<input
id="quote-quantity"
type="range"
min="1"
max="20"
r-model.number="quantity"
:value="quantity"
:disabled="!interactive"
/>
<label for="quote-price"
>Price per seat <strong>{{ money(unitPrice) }}</strong></label
>
<input
id="quote-price"
type="range"
min="10"
max="100"
step="5"
r-model.number="unitPrice"
:value="unitPrice"
:disabled="!interactive"
/>
<label class="guide-check"
><input type="checkbox" r-model="discounted" :disabled="!interactive" />
Apply a 10% team discount</label
>
<button type="button" @click="reset" :disabled="!interactive">
Reset quote
</button>
</div>
<div class="guide-readout">
<span class="guide-kicker">YOUR TEAM / MONTHLY</span>
<output class="guide-total" aria-live="polite">{{ money(total) }}</output>
<dl>
<div>
<dt>Subtotal</dt>
<dd>{{ money(subtotal) }}</dd>
</div>
<div>
<dt>Discount</dt>
<dd>{{ money(savings) }}</dd>
</div>
</dl>
<p>Two inputs. Three computed values. One connected view.</p>
</div>
</div>` import { createApp, useScope } from 'regor'
import { createQuote, quoteTemplate } from './quote'
import { createServices, servicesTemplate } from './services'
import { createProfile, profileTemplate } from './profile'
import { createLifecycle, lifecycleTemplate } from './lifecycle'
// Each guide mounts only its own island. Everything outside the root stays static.
function mount<T extends object>(
id: string,
create: (interactive: boolean) => T,
template: string,
) {
const element = document.getElementById(id)
if (element)
createApp(
useScope(() => create(true)),
{ element, template },
)
}
mount('guide-quote', createQuote, quoteTemplate)
mount('guide-services', createServices, servicesTemplate)
mount('guide-profile', createProfile, profileTemplate)
mount('guide-lifecycle', createLifecycle, lifecycleTemplate) Move either slider or apply the discount. The subtotal, savings, and total share the same reactive state.
Core model
- A ref is a callable value container (
r(),r(newValue)) with.valuealias. ref(),cref(), andsref()are all refs (isRef(...) === true).ref()is deep-conversion oriented.cref()is copy-first deep-conversion oriented.sref()is shallow-conversion oriented.
ref vs cref vs sref (important)
ref(value)
ref recursively converts nested object/array properties to refs.
const user = ref({ name: 'Ada', meta: { active: true } })
user().name('Grace')
user().meta().active(false) Key behavior:
- Object/array content is converted recursively.
- Source objects are mutated in place during conversion.
- Existing refs are reused.
Node,Date,RegExp,Promise,Errorare not recursively converted.
cref(value)
cref first flattens the value into a plain copied structure, then recursively converts that copy to refs.
const source = { user: { name: 'Ada' } }
const user = cref(source)
user().user().name('Grace')
source.user.name // Ada Key behavior:
- Equivalent to
ref(flatten(value)). - Source objects are not mutated during initial conversion.
- Nested refs are unwrapped into the copied structure before deep conversion.
- It costs more than
ref, so preferrefwhere in-place conversion is acceptable.
sref(value)
sref keeps nested properties as plain values (no recursive wrapping).
const user = sref({ name: 'Ada', age: 30 })
user().name = 'Grace'
user().age = 31 Key behavior:
- Nested values stay plain unless they were already refs.
- Arrays/Map/Set are made reactive via prototype adaptation.
sref(sourceRef)returns the same ref instance.
Choose cref() when you need a copy, or sref() when you want to replace plain values yourself.
Access and update forms
For ref, cref, and sref:
const r = ref(1)
r() // get
r(2) // set
r.value // get
r.value = 3 // set Derived refs: computed, computeRef, computeMany
Regor computed refs are:
- Read-only.
- Lazy (first evaluation happens on first read).
- Cached until dependencies change.
- Invalidated on source change, then recomputed on next read.
const a = ref(1)
const b = ref(2)
const sum = computeMany([a, b], (x, y) => x + y)
sum() // 3 (computes now)
sum() // 3 (cached)
a(5)
sum() // 7 (recomputed after invalidation) watchEffect
watchEffect immediately runs and tracks refs accessed during execution.
When any tracked ref changes:
- Cleanup callbacks registered through
onCleanupare called. - Effect is re-subscribed using newly accessed refs.
const count = ref(0)
const stop = watchEffect((onCleanup) => {
const snapshot = count()
onCleanup?.(() => console.log('cleanup', snapshot))
console.log('value', snapshot)
}) Use silence(() => ...) to read refs without tracking.
observe and observeMany
observe(source, cb, init?) listens to one ref. observeMany([a, b], cb, init?) listens to multiple refs and returns tuple values.
Both return a stop function.
Batch and manual trigger
batch/startBatch/endBatch defer observer notification until batch end.
batch(() => {
a(1)
a(2)
b(3)
}) Observers are triggered once per changed ref after batch completion.
Use trigger(ref) to manually notify observers.
trigger(ref, undefined, true) recursively triggers nested refs.
Pause and resume
pause(ref) disables auto observer firing for that ref. resume(ref) re-enables it. Manual trigger(ref) still works while paused.
Other important helpers
unref(x)unwraps one ref level.isRef(x)checks any ref (ref,cref, orsref).isDeepRef(x)checks whether value was created as deepref.flatten(x)recursively unwraps refs in objects/arrays/Map/Set into plain data.entangle(a, b)creates two-way sync and initializesbwitha.persist(ref, key)syncs ref data withlocalStorage.
flatten sample
Use flatten when you need a plain snapshot for logging, serialization, or transport.
const state = ref({
user: ref({ name: 'Ada' }),
tags: new Set([ref('core')]),
meta: new Map([['count', ref(2)]]),
})
const plain = flatten(state)
// {
// user: { name: 'Ada' },
// tags: Set { 'core' },
// meta: Map { 'count' => 2 }
// } Collections and caveats
MapandSetare reactive through Regor’s proxy prototypes.WeakMapandWeakSetare not auto-reactive; usetrigger(...)manually when needed.- For very large nested structures, pick
ref,cref, orsrefintentionally based on mutation policy, update style, and cost.