place

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.

a
2
b
3
c (a + b)
5
watch() effect log
  • 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.

ts
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.

ts
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.

ts
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:

ts
// Plain functions that read state also "derive" — but they recompute // on every call. Reach for derived() when you want caching. const c = () => a() + b()
No useMemo equivalent
place's 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

ts
// 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:

ts
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.

width640px
height800px
breakpointsm
reduced motionno
prefers darkno
ts
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>
When to use which
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.

See also