Reactivity
place's reactivity is fine-grained signals — read a state, you depend on it; write to it, only the dependents recompute. No virtual DOM, no per-tick reconciliation, no diffing.
Try it
Below is a tiny graph: two source states (a, b), a derived valuec = a + b, and a watch effect logging every recomputation. Click the buttons — the framework's own reactive primitives are driving the page you're reading.
- c = a + b = 5
- c = a + b = 5
state()
A writable cell. read() samples (and tracks if inside a reactive context);write() updates and notifies subscribers.
import { state } from '@place/reactivity'
const count = state(0)
count() // 0
count.set(1)
count() // 1
watch()
A reactive effect. Re-runs whenever any state it read changes. Returns a disposer; auto- disposes inside component scope.
import { state, watch } from '@place/reactivity'
const a = state(2)
const b = state(3)
watch(() => {
console.log('c =', a() + b()) // logs whenever a or b change
})
a.set(5) // logs "c = 8"
b.set(1) // logs "c = 6"
derived()
Memoized derived value. Wraps a function and caches the last result; recomputes only when a tracked dependency changes value.
import { derived, state } from '@place/component'
const a = state(2)
const b = state(3)
// derived(fn) returns a memoized () => T accessor. It tracks a + b
// and recomputes only when one of them changes value.
const c = derived(() => a() + b())
c() // 5 — computed
c() // 5 — cached (no recomputation)
a.set(10)
c() // 13 — recomputed exactly once
For ad-hoc derivations that aren't read more than once per pass, a plain function works too — the reactive graph still wires up, you just pay the recomputation each call:
// Plain functions that read state also "derive" — but they recompute
// on every call. Reach for derived() when you want caching.
const c = () => a() + b()
derived() is the only memo primitive you'll need. There's no separate "computed", no dependency array, no equality function to pass — the graph already knows which sources changed.Two-color propagation
// Two-color graph propagation. When a writes, dependents go RED
// (dirty); reads pull them through and they go BLACK (clean). A
// dependent that re-reads to the same value short-circuits — its
// downstream stays clean. This is the same algorithm TC39 standardizes
// for native signals.
Each node has a color: black (clean) or red (dirty). A write paints all dependents red; a read pulls a red node back to black by recomputing. The algorithm is the one TC39 picked for the signals proposal — when native signals ship, this code maps to them directly.
Batching
Multiple writes inside batch() trigger one downstream flush:
import { batch } from '@place/reactivity'
batch(() => {
a.set(10)
b.set(20)
}) // watchers run once, after the batch
viewport — reactive screen size
The framework ships one viewport primitive every component subscribes to instead of wiring its own matchMedia / ResizeObserver. Resize your browser; the readout below updates without a page reload.
import { viewport } from '@place/component'
viewport.width() // () => number
viewport.height() // () => number
viewport.breakpoint() // () => 'sm' | 'md' | 'lg' | 'xl' | '2xl'
viewport.prefersReducedMotion() // () => boolean
viewport.prefersDark() // () => boolean
viewport.matches('(min-width: 800px)') // (q) => () => boolean
// Behavioural responsiveness (use this for "which component to render"):
{viewport.breakpoint() === 'sm' ? <MobileDrawer /> : <Sidebar />}
// Stylistic responsiveness (use Tailwind to avoid flash on hydrate):
<div class="hidden md:block">…</div>
viewport.* is for behavioural responsiveness — picking a component to render based on screen size. Tailwind utilities (sm: / md: / lg:) are for stylistic responsiveness — the CSS itself is media-query-based so there's no JS-driven flash on hydrate. Combine freely.