# tilia Compact machine guide for tilia, the reactive state core for TypeScript and ReScript. ## Exact API contracts These files are authoritative. If prose differs, follow the contracts. Repository: - TypeScript: `./src/index.d.ts` - ReScript: `./src/Tilia.resi` Installed npm package: - TypeScript: `./dist/index.d.ts` - ReScript: `./src/Tilia.resi` Published documentation: - Guide: - API: - TypeScript contract: - ReScript contract: ## Mental model - `tilia` proxies plain objects and arrays without changing their public shape. - Reads inside reactive callbacks are dependencies; writes notify only readers of the affected keys. - `observe` is a tracked effect. It runs immediately and again after tracked values change. - `watch` separates a tracked capture function from an untracked effect. The capture runs initially; the effect starts only after a dependency changes. - `computed`, `source`, and `store` define dynamic values for insertion into a `tilia` or `carve` object. - `signal` is a standalone reactive value. Write through its returned setter. - `make` creates an isolated reactive context; top-level functions use the default context. ## Choosing the API - Use `tilia(object)` for a plain reactive object or array. - Use `carve(({derived}) => object)` when fields or methods need the complete object as `self`. Keep the factory declarative; move domain logic into focused functions and bind them with `derived`. - Use `computed(fn)` for a pull-based cached field. - Use `observe(fn)` for one tracked effect. - Use `watch(capture, effect)` when reads and side effects should be separate. - Use `signal(value)` for standalone mutable state, `derived(fn)` for a standalone derived signal, and `lift(signal)` to insert its value. - Use `source(initial, setup)` when external or asynchronous work emits values. - Use `store(setup)` when setup both constructs the initial value and owns later updates. - Use `readonly(data)` to keep immutable or foreign data unproxied. - Use `batch(fn)` to group writes made outside a tilia callback. ## Critical rules - Insert `computed`, `source`, and `store` directly into a reactive object. Reading an orphan computed value raises. - `source` tracks only synchronous reads made by `setup(previous, set)`. Read dependencies before starting promises or other asynchronous work. - `previous` is the latest emitted source value. It remains visible until `set(next)` runs, enabling stale-while-revalidate behavior. - `observe` and `watch` return cancellation functions. - A `watch` effect is untracked and may read unrelated reactive state. - Tilia callbacks already defer notification flushing; an extra `batch` is normally unnecessary inside them. - `readonly(data)` returns a holder whose `data` field is non-writable and not tracked or proxied. ## Minimal TypeScript ```typescript import { carve, observe } from "tilia"; type Counter = { value: number; double: number; increment: () => void; }; const counter = carve(({ derived }) => ({ value: 1, double: derived((self) => self.value * 2), increment: derived((self) => () => { self.value++; }), })); const cancel = observe(() => console.log(counter.double)); counter.increment(); cancel(); ``` ## Minimal ReScript ```rescript open Tilia type counter = { mutable value: int, double: int, increment: unit => unit, } let counter: counter = carve(({derived}) => { value: 1, double: derived(self => self.value * 2), increment: derived(self => () => self.value = self.value + 1), }) let cancel = observe(() => Console.log(counter.double)) counter.increment() cancel() ``` ## React React integration lives in `@tilia/react`. - TypeScript contract: `../react/src/index.d.ts` - ReScript contract: `../react/src/TiliaReact.resi` - API: